commit
179bcff88f
@ -1,69 +0,0 @@ |
||||
{ |
||||
"project_info": { |
||||
"project_number": "453392274790", |
||||
"firebase_url": "https://legado-fca69.firebaseio.com", |
||||
"project_id": "legado-fca69", |
||||
"storage_bucket": "legado-fca69.appspot.com" |
||||
}, |
||||
"client": [ |
||||
{ |
||||
"client_info": { |
||||
"mobilesdk_app_id": "1:453392274790:android:1d2b1eefbe0e78cff624a7", |
||||
"android_client_info": { |
||||
"package_name": "io.legado.app" |
||||
} |
||||
}, |
||||
"oauth_client": [ |
||||
{ |
||||
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", |
||||
"client_type": 3 |
||||
} |
||||
], |
||||
"api_key": [ |
||||
{ |
||||
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" |
||||
} |
||||
], |
||||
"services": { |
||||
"appinvite_service": { |
||||
"other_platform_oauth_client": [ |
||||
{ |
||||
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", |
||||
"client_type": 3 |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
"client_info": { |
||||
"mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7", |
||||
"android_client_info": { |
||||
"package_name": "io.legado.app.debug" |
||||
} |
||||
}, |
||||
"oauth_client": [ |
||||
{ |
||||
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", |
||||
"client_type": 3 |
||||
} |
||||
], |
||||
"api_key": [ |
||||
{ |
||||
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" |
||||
} |
||||
], |
||||
"services": { |
||||
"appinvite_service": { |
||||
"other_platform_oauth_client": [ |
||||
{ |
||||
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", |
||||
"client_type": 3 |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
], |
||||
"configuration_version": "1" |
||||
} |
@ -1,56 +1,62 @@ |
||||
[ |
||||
{ |
||||
"enable": true, |
||||
"name": "数字 分隔符 标题名称", |
||||
"rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", |
||||
"serialNumber": 0 |
||||
"enable": true, |
||||
"name": "数字 分隔符 标题名称", |
||||
"rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", |
||||
"serialNumber": 0 |
||||
}, |
||||
{ |
||||
"enable": true, |
||||
"name": "特殊符号 序号 标题", |
||||
"rule": "^[ \\t]{0,4}[\\[〈「『〖〔《(【\\(]?[第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[\\.:: \f\t].{0,30}$", |
||||
"serialNumber": 1 |
||||
"enable": true, |
||||
"name": "目录", |
||||
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||
"serialNumber": 1 |
||||
}, |
||||
{ |
||||
"enable": true, |
||||
"name": "特殊符号 标题", |
||||
"rule": "^[ \\t]{0,4}[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$", |
||||
"serialNumber": 2 |
||||
"enable": false, |
||||
"name": "目录(不匹配行前空白)", |
||||
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||
"serialNumber": 2 |
||||
}, |
||||
{ |
||||
"enable": false, |
||||
"name": "特殊符号 标题(不匹配空白字符)", |
||||
"rule": "(?<=\\s)[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$", |
||||
"serialNumber": 3 |
||||
"enable": false, |
||||
"name": "目录(去简介)", |
||||
"rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||
"serialNumber": 3 |
||||
}, |
||||
{ |
||||
"enable": true, |
||||
"name": "Chapter/Section/Part 序号 标题", |
||||
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$", |
||||
"serialNumber": 4 |
||||
"enable": false, |
||||
"name": "目录(古典小说备用)", |
||||
"rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$", |
||||
"serialNumber": 4 |
||||
}, |
||||
{ |
||||
"enable": true, |
||||
"name": "正文 标题/序号", |
||||
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$", |
||||
"serialNumber": 5 |
||||
"enable": true, |
||||
"name": "Chapter/Section/Part 序号 标题", |
||||
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$", |
||||
"serialNumber": 5 |
||||
}, |
||||
{ |
||||
"enable": true, |
||||
"name": "目录", |
||||
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||
"serialNumber": 6 |
||||
"enable": true, |
||||
"name": "正文 标题/序号", |
||||
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$", |
||||
"serialNumber": 6 |
||||
}, |
||||
{ |
||||
"enable": false, |
||||
"name": "目录(不匹配行前空白,正文不出现双标题)", |
||||
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||
"serialNumber": 7 |
||||
"enable": true, |
||||
"name": "特殊符号 序号 标题", |
||||
"rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$", |
||||
"serialNumber": 7 |
||||
}, |
||||
{ |
||||
"enable": false, |
||||
"name": "目录(古典小说备用)", |
||||
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$", |
||||
"serialNumber": 8 |
||||
"enable": true, |
||||
"name": "特殊符号 标题", |
||||
"rule": "^[ \\t]{0,4}[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", |
||||
"serialNumber": 8 |
||||
}, |
||||
{ |
||||
"enable":false, |
||||
"name": "特殊符号 标题(不匹配空白字符)", |
||||
"rule": "(?<=\\s)[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", |
||||
"serialNumber": 9 |
||||
} |
||||
] |
||||
|
@ -1,34 +0,0 @@ |
||||
package io.legado.app.help |
||||
|
||||
import androidx.recyclerview.widget.ListUpdateCallback |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import io.legado.app.base.adapter.ItemViewHolder |
||||
|
||||
class FirstTopListUpCallback : ListUpdateCallback { |
||||
var firstInsert = -1 |
||||
lateinit var adapter: RecyclerView.Adapter<ItemViewHolder> |
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) { |
||||
adapter.notifyItemRangeChanged(position, count, payload) |
||||
} |
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) { |
||||
if (toPosition == 0) { |
||||
firstInsert = 0 |
||||
} |
||||
adapter.notifyItemMoved(fromPosition, toPosition) |
||||
} |
||||
|
||||
override fun onInserted(position: Int, count: Int) { |
||||
if (firstInsert == -1 || firstInsert > position) { |
||||
firstInsert = position |
||||
} |
||||
adapter.notifyItemRangeInserted(position, count) |
||||
} |
||||
|
||||
override fun onRemoved(position: Int, count: Int) { |
||||
adapter.notifyItemRangeRemoved(position, count) |
||||
} |
||||
|
||||
|
||||
} |
@ -0,0 +1,173 @@ |
||||
package io.legado.app.ui.book.read |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.app.SearchManager |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.content.pm.ResolveInfo |
||||
import android.net.Uri |
||||
import android.os.Build |
||||
import android.view.LayoutInflater |
||||
import android.view.Menu |
||||
import android.view.ViewGroup |
||||
import android.widget.PopupWindow |
||||
import androidx.annotation.RequiresApi |
||||
import androidx.appcompat.view.SupportMenuInflater |
||||
import androidx.appcompat.view.menu.MenuBuilder |
||||
import androidx.appcompat.view.menu.MenuItemImpl |
||||
import androidx.appcompat.widget.PopupMenu |
||||
import androidx.core.view.size |
||||
import androidx.recyclerview.widget.LinearLayoutManager |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import io.legado.app.R |
||||
import io.legado.app.base.adapter.ItemViewHolder |
||||
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
||||
import io.legado.app.utils.isAbsUrl |
||||
import io.legado.app.utils.sendToClip |
||||
import io.legado.app.utils.visible |
||||
import kotlinx.android.synthetic.main.item_fillet_text.view.* |
||||
import kotlinx.android.synthetic.main.popup_action_menu.view.* |
||||
import org.jetbrains.anko.sdk27.listeners.onClick |
||||
import org.jetbrains.anko.share |
||||
import org.jetbrains.anko.toast |
||||
|
||||
@SuppressLint("RestrictedApi") |
||||
class TextActionMenu(private val context: Context, private val callBack: CallBack) : |
||||
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) { |
||||
|
||||
init { |
||||
@SuppressLint("InflateParams") |
||||
contentView = LayoutInflater.from(context).inflate(R.layout.popup_action_menu, null) |
||||
|
||||
isTouchable = true |
||||
isOutsideTouchable = false |
||||
isFocusable = false |
||||
|
||||
initRecyclerView() |
||||
} |
||||
|
||||
private fun initRecyclerView() = with(contentView) { |
||||
val adapter = Adapter(context) |
||||
recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) |
||||
recycler_view.adapter = adapter |
||||
val menu = MenuBuilder(context) |
||||
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) |
||||
adapter.setItems(menu.visibleItems) |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
||||
val popupMenu = PopupMenu(context, iv_menu_more) |
||||
onInitializeMenu(popupMenu.menu) |
||||
if (popupMenu.menu.size > 0) { |
||||
iv_menu_more.visible() |
||||
popupMenu.setOnMenuItemClickListener { item -> |
||||
item.intent?.let { |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
||||
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText) |
||||
context.startActivity(it) |
||||
} |
||||
} |
||||
this@TextActionMenu.dismiss() |
||||
true |
||||
} |
||||
} |
||||
iv_menu_more.onClick { |
||||
popupMenu.show() |
||||
} |
||||
} |
||||
} |
||||
|
||||
inner class Adapter(context: Context) : |
||||
SimpleRecyclerAdapter<MenuItemImpl>(context, R.layout.item_text) { |
||||
|
||||
override fun convert( |
||||
holder: ItemViewHolder, |
||||
item: MenuItemImpl, |
||||
payloads: MutableList<Any> |
||||
) { |
||||
with(holder.itemView) { |
||||
text_view.text = item.title |
||||
} |
||||
} |
||||
|
||||
override fun registerListener(holder: ItemViewHolder) { |
||||
holder.itemView.onClick { |
||||
getItem(holder.layoutPosition)?.let { |
||||
if (!callBack.onMenuItemSelected(it.itemId)) { |
||||
onMenuItemSelected(it) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun onMenuItemSelected(item: MenuItemImpl) { |
||||
when (item.itemId) { |
||||
R.id.menu_copy -> context.sendToClip(callBack.selectedText) |
||||
R.id.menu_share_str -> context.share(callBack.selectedText) |
||||
R.id.menu_browser -> { |
||||
try { |
||||
val intent = if (callBack.selectedText.isAbsUrl()) { |
||||
Intent(Intent.ACTION_VIEW).apply { |
||||
data = Uri.parse(callBack.selectedText) |
||||
} |
||||
} else { |
||||
Intent(Intent.ACTION_WEB_SEARCH).apply { |
||||
putExtra(SearchManager.QUERY, callBack.selectedText) |
||||
} |
||||
} |
||||
context.startActivity(intent) |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
context.toast(e.localizedMessage ?: "ERROR") |
||||
} |
||||
} |
||||
} |
||||
callBack.onMenuActionFinally() |
||||
} |
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M) |
||||
private fun createProcessTextIntent(): Intent { |
||||
return Intent() |
||||
.setAction(Intent.ACTION_PROCESS_TEXT) |
||||
.setType("text/plain") |
||||
} |
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M) |
||||
private fun getSupportedActivities(): List<ResolveInfo> { |
||||
return context.packageManager |
||||
.queryIntentActivities(createProcessTextIntent(), 0) |
||||
} |
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M) |
||||
private fun createProcessTextIntentForResolveInfo(info: ResolveInfo): Intent { |
||||
return createProcessTextIntent() |
||||
.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) |
||||
.setClassName(info.activityInfo.packageName, info.activityInfo.name) |
||||
} |
||||
|
||||
/** |
||||
* Start with a menu Item order value that is high enough |
||||
* so that your "PROCESS_TEXT" menu items appear after the |
||||
* standard selection menu items like Cut, Copy, Paste. |
||||
*/ |
||||
@RequiresApi(Build.VERSION_CODES.M) |
||||
private fun onInitializeMenu(menu: Menu) { |
||||
// Start with a menu Item order value that is high enough |
||||
// so that your "PROCESS_TEXT" menu items appear after the |
||||
// standard selection menu items like Cut, Copy, Paste. |
||||
var menuItemOrder = 100 |
||||
for (resolveInfo in getSupportedActivities()) { |
||||
menu.add( |
||||
Menu.NONE, Menu.NONE, |
||||
menuItemOrder++, resolveInfo.loadLabel(context.packageManager) |
||||
).intent = createProcessTextIntentForResolveInfo(resolveInfo) |
||||
} |
||||
} |
||||
|
||||
interface CallBack { |
||||
val selectedText: String |
||||
|
||||
fun onMenuItemSelected(itemId: Int): Boolean |
||||
|
||||
fun onMenuActionFinally() |
||||
} |
||||
} |
@ -0,0 +1,259 @@ |
||||
package io.legado.app.ui.book.read.config |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.content.Context |
||||
import android.os.Bundle |
||||
import android.util.DisplayMetrics |
||||
import android.view.LayoutInflater |
||||
import android.view.MenuItem |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
import androidx.appcompat.widget.Toolbar |
||||
import androidx.fragment.app.FragmentManager |
||||
import androidx.lifecycle.LiveData |
||||
import androidx.lifecycle.Observer |
||||
import androidx.recyclerview.widget.ItemTouchHelper |
||||
import androidx.recyclerview.widget.LinearLayoutManager |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import io.legado.app.App |
||||
import io.legado.app.R |
||||
import io.legado.app.base.BaseDialogFragment |
||||
import io.legado.app.base.adapter.ItemViewHolder |
||||
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
||||
import io.legado.app.constant.Theme |
||||
import io.legado.app.data.entities.TxtTocRule |
||||
import io.legado.app.help.ItemTouchCallback |
||||
import io.legado.app.lib.dialogs.alert |
||||
import io.legado.app.lib.dialogs.cancelButton |
||||
import io.legado.app.lib.dialogs.customView |
||||
import io.legado.app.lib.dialogs.okButton |
||||
import io.legado.app.model.localBook.AnalyzeTxtFile |
||||
import io.legado.app.ui.widget.recycler.VerticalDivider |
||||
import io.legado.app.utils.applyTint |
||||
import kotlinx.android.synthetic.main.dialog_toc_regex.* |
||||
import kotlinx.android.synthetic.main.dialog_toc_regex_edit.view.* |
||||
import kotlinx.android.synthetic.main.item_toc_regex.view.* |
||||
import kotlinx.coroutines.Dispatchers.IO |
||||
import kotlinx.coroutines.launch |
||||
import org.jetbrains.anko.sdk27.listeners.onClick |
||||
import java.util.* |
||||
|
||||
|
||||
class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { |
||||
|
||||
private lateinit var adapter: TocRegexAdapter |
||||
private var tocRegexLiveData: LiveData<List<TxtTocRule>>? = null |
||||
var selectedName: String? = null |
||||
private var durRegex: String? = null |
||||
|
||||
override fun onStart() { |
||||
super.onStart() |
||||
val dm = DisplayMetrics() |
||||
activity?.windowManager?.defaultDisplay?.getMetrics(dm) |
||||
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt()) |
||||
} |
||||
|
||||
override fun onCreateView( |
||||
inflater: LayoutInflater, |
||||
container: ViewGroup?, |
||||
savedInstanceState: Bundle? |
||||
): View? { |
||||
return inflater.inflate(R.layout.dialog_toc_regex, container) |
||||
} |
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
||||
super.onViewCreated(view, savedInstanceState) |
||||
durRegex = arguments?.getString("tocRegex") |
||||
tool_bar.setTitle(R.string.txt_toc_regex) |
||||
tool_bar.inflateMenu(R.menu.txt_toc_regex) |
||||
tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) |
||||
tool_bar.setOnMenuItemClickListener(this) |
||||
initView() |
||||
initData() |
||||
} |
||||
|
||||
private fun initView() { |
||||
adapter = TocRegexAdapter(requireContext()) |
||||
recycler_view.layoutManager = LinearLayoutManager(requireContext()) |
||||
recycler_view.addItemDecoration(VerticalDivider(requireContext())) |
||||
recycler_view.adapter = adapter |
||||
val itemTouchCallback = ItemTouchCallback() |
||||
itemTouchCallback.onItemTouchCallbackListener = adapter |
||||
itemTouchCallback.isCanDrag = true |
||||
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) |
||||
tv_cancel.onClick { |
||||
dismiss() |
||||
} |
||||
tv_ok.onClick { |
||||
adapter.getItems().forEach { tocRule -> |
||||
if (selectedName == tocRule.name) { |
||||
val callBack = activity as? CallBack |
||||
callBack?.onTocRegexDialogResult(tocRule.rule) |
||||
dismiss() |
||||
return@onClick |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun initData() { |
||||
tocRegexLiveData?.removeObservers(viewLifecycleOwner) |
||||
tocRegexLiveData = App.db.txtTocRule().observeAll() |
||||
tocRegexLiveData?.observe(viewLifecycleOwner, Observer { tocRules -> |
||||
initSelectedName(tocRules) |
||||
adapter.setItems(tocRules) |
||||
}) |
||||
} |
||||
|
||||
private fun initSelectedName(tocRules: List<TxtTocRule>) { |
||||
if (selectedName == null && durRegex != null) { |
||||
tocRules.forEach { |
||||
if (durRegex == it.rule) { |
||||
selectedName = it.name |
||||
return@forEach |
||||
} |
||||
} |
||||
if (selectedName == null) { |
||||
selectedName = "" |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean { |
||||
when (item?.itemId) { |
||||
R.id.menu_add -> editRule() |
||||
R.id.menu_default -> importDefault() |
||||
} |
||||
return false |
||||
} |
||||
|
||||
private fun importDefault() { |
||||
launch(IO) { |
||||
AnalyzeTxtFile.getDefaultRules().let { |
||||
App.db.txtTocRule().insert(*it.toTypedArray()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("InflateParams") |
||||
private fun editRule(rule: TxtTocRule? = null) { |
||||
val tocRule = rule?.copy() ?: TxtTocRule() |
||||
requireContext().alert(titleResource = R.string.txt_toc_regex) { |
||||
var rootView: View? = null |
||||
customView { |
||||
LayoutInflater.from(requireContext()) |
||||
.inflate(R.layout.dialog_toc_regex_edit, null).apply { |
||||
rootView = this |
||||
tv_rule_name.setText(tocRule.name) |
||||
tv_rule_regex.setText(tocRule.rule) |
||||
} |
||||
} |
||||
okButton { |
||||
rootView?.apply { |
||||
tocRule.name = tv_rule_name.text.toString() |
||||
tocRule.rule = tv_rule_regex.text.toString() |
||||
saveRule(tocRule, rule) |
||||
} |
||||
} |
||||
cancelButton() |
||||
}.show().applyTint() |
||||
} |
||||
|
||||
private fun saveRule(rule: TxtTocRule, oldRule: TxtTocRule? = null) { |
||||
launch(IO) { |
||||
if (rule.serialNumber < 0) { |
||||
rule.serialNumber = adapter.getItems().lastOrNull()?.serialNumber ?: 0 + 1 |
||||
} |
||||
oldRule?.let { |
||||
App.db.txtTocRule().delete(oldRule) |
||||
} |
||||
App.db.txtTocRule().insert(rule) |
||||
} |
||||
} |
||||
|
||||
inner class TocRegexAdapter(context: Context) : |
||||
SimpleRecyclerAdapter<TxtTocRule>(context, R.layout.item_toc_regex), |
||||
ItemTouchCallback.OnItemTouchCallbackListener { |
||||
|
||||
override fun convert(holder: ItemViewHolder, item: TxtTocRule, payloads: MutableList<Any>) { |
||||
holder.itemView.apply { |
||||
if (payloads.isEmpty()) { |
||||
rb_regex_name.text = item.name |
||||
rb_regex_name.isChecked = item.name == selectedName |
||||
swt_enabled.isChecked = item.enable |
||||
} else { |
||||
rb_regex_name.isChecked = item.name == selectedName |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun registerListener(holder: ItemViewHolder) { |
||||
holder.itemView.apply { |
||||
rb_regex_name.setOnCheckedChangeListener { buttonView, isChecked -> |
||||
if (buttonView.isPressed && isChecked) { |
||||
selectedName = getItem(holder.layoutPosition)?.name |
||||
updateItems(0, itemCount - 1, true) |
||||
} |
||||
} |
||||
swt_enabled.setOnCheckedChangeListener { buttonView, isChecked -> |
||||
if (buttonView.isPressed) { |
||||
getItem(holder.layoutPosition)?.let { |
||||
it.enable = isChecked |
||||
launch(IO) { |
||||
App.db.txtTocRule().update(it) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
iv_edit.onClick { |
||||
editRule(getItem(holder.layoutPosition)) |
||||
} |
||||
iv_delete.onClick { |
||||
getItem(holder.layoutPosition)?.let { item -> |
||||
launch(IO) { |
||||
App.db.txtTocRule().delete(item) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private var isMoved = false |
||||
|
||||
override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { |
||||
Collections.swap(getItems(), srcPosition, targetPosition) |
||||
notifyItemMoved(srcPosition, targetPosition) |
||||
isMoved = true |
||||
return super.onMove(srcPosition, targetPosition) |
||||
} |
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { |
||||
super.clearView(recyclerView, viewHolder) |
||||
if (isMoved) { |
||||
for ((index, item) in getItems().withIndex()) { |
||||
item.serialNumber = index + 1 |
||||
} |
||||
launch(IO) { |
||||
App.db.txtTocRule().update(*getItems().toTypedArray()) |
||||
} |
||||
} |
||||
isMoved = false |
||||
} |
||||
} |
||||
|
||||
|
||||
companion object { |
||||
fun show(fragmentManager: FragmentManager, tocRegex: String? = null) { |
||||
val dialog = TocRegexDialog() |
||||
val bundle = Bundle() |
||||
bundle.putString("tocRegex", tocRegex) |
||||
dialog.arguments = bundle |
||||
dialog.show(fragmentManager, "tocRegexDialog") |
||||
} |
||||
} |
||||
|
||||
interface CallBack { |
||||
fun onTocRegexDialogResult(tocRegex: String) {} |
||||
} |
||||
|
||||
} |
@ -1,39 +0,0 @@ |
||||
package io.legado.app.ui.book.read.page |
||||
|
||||
import android.view.ActionMode |
||||
import android.view.Menu |
||||
import android.view.MenuItem |
||||
import android.widget.TextView |
||||
|
||||
import io.legado.app.R |
||||
import io.legado.app.constant.EventBus |
||||
import io.legado.app.utils.postEvent |
||||
|
||||
class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback { |
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { |
||||
when (item?.itemId) { |
||||
R.id.menu_replace -> { |
||||
val text = textView.text.substring(textView.selectionStart, textView.selectionEnd) |
||||
postEvent(EventBus.REPLACE, text) |
||||
mode?.finish() |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { |
||||
mode?.menuInflater?.inflate(R.menu.content_select_action, menu) |
||||
return true |
||||
} |
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { |
||||
return false |
||||
} |
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) { |
||||
|
||||
} |
||||
|
||||
} |
@ -1,124 +1,591 @@ |
||||
package io.legado.app.ui.book.read.page |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.content.Context |
||||
import android.graphics.Canvas |
||||
import android.graphics.Paint |
||||
import android.graphics.RectF |
||||
import android.util.AttributeSet |
||||
import android.view.MotionEvent |
||||
import android.view.View |
||||
import io.legado.app.R |
||||
import io.legado.app.constant.PreferKey |
||||
import io.legado.app.help.ReadBookConfig |
||||
import io.legado.app.lib.theme.accentColor |
||||
import io.legado.app.ui.book.read.page.entities.SelectPoint |
||||
import io.legado.app.ui.book.read.page.entities.TextChar |
||||
import io.legado.app.ui.book.read.page.entities.TextPage |
||||
import io.legado.app.utils.activity |
||||
import io.legado.app.utils.getCompatColor |
||||
import io.legado.app.utils.getPrefBoolean |
||||
|
||||
|
||||
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { |
||||
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble) |
||||
var upView: ((TextPage) -> Unit)? = null |
||||
private val selectedPaint by lazy { |
||||
Paint().apply { |
||||
color = context.getCompatColor(R.color.btn_bg_press_2) |
||||
style = Paint.Style.FILL |
||||
} |
||||
} |
||||
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble) |
||||
var selectStartLine = 0 |
||||
var selectStartChar = 0 |
||||
var selectEndLine = 0 |
||||
var selectEndChar = 0 |
||||
private var textPage: TextPage? = null |
||||
private var callBack: CallBack |
||||
private val visibleRect = RectF() |
||||
private val selectStart = arrayOf(0, 0, 0) |
||||
private val selectEnd = arrayOf(0, 0, 0) |
||||
private var textPage: TextPage = TextPage() |
||||
//滚动参数 |
||||
private val pageFactory: TextPageFactory get() = callBack.pageFactory |
||||
private val maxScrollOffset = 100f |
||||
private var pageOffset = 0f |
||||
|
||||
init { |
||||
callBack = activity as CallBack |
||||
contentDescription = textPage.text |
||||
} |
||||
|
||||
fun setContent(textPage: TextPage?) { |
||||
fun setContent(textPage: TextPage) { |
||||
this.textPage = textPage |
||||
contentDescription = textPage.text |
||||
invalidate() |
||||
} |
||||
|
||||
fun upVisibleRect() { |
||||
visibleRect.set( |
||||
ChapterProvider.paddingLeft.toFloat(), |
||||
ChapterProvider.paddingTop.toFloat(), |
||||
ChapterProvider.visibleRight.toFloat(), |
||||
ChapterProvider.visibleBottom.toFloat() |
||||
) |
||||
} |
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { |
||||
super.onSizeChanged(w, h, oldw, oldh) |
||||
ReadBookConfig.durConfig.let { |
||||
ChapterProvider.viewWidth = w |
||||
ChapterProvider.viewHeight = h |
||||
ChapterProvider.upSize(ReadBookConfig.durConfig) |
||||
ChapterProvider.viewWidth = w |
||||
ChapterProvider.viewHeight = h |
||||
ChapterProvider.upSize() |
||||
upVisibleRect() |
||||
textPage.format() |
||||
} |
||||
|
||||
override fun onDraw(canvas: Canvas) { |
||||
super.onDraw(canvas) |
||||
canvas.clipRect(visibleRect) |
||||
drawPage(canvas) |
||||
} |
||||
|
||||
/** |
||||
* 绘制页面 |
||||
*/ |
||||
private fun drawPage(canvas: Canvas) { |
||||
var relativeOffset = relativeOffset(0) |
||||
textPage.textLines.forEach { textLine -> |
||||
val lineTop = textLine.lineTop + relativeOffset |
||||
val lineBase = textLine.lineBase + relativeOffset |
||||
val lineBottom = textLine.lineBottom + relativeOffset |
||||
drawChars( |
||||
canvas, |
||||
textLine.textChars, |
||||
lineTop, |
||||
lineBase, |
||||
lineBottom, |
||||
textLine.isTitle, |
||||
textLine.isReadAloud |
||||
) |
||||
} |
||||
if (!ReadBookConfig.isScroll) return |
||||
//滚动翻页 |
||||
val nextPage = relativePage(1) |
||||
relativeOffset = relativeOffset(1) |
||||
nextPage.textLines.forEach { textLine -> |
||||
val lineTop = textLine.lineTop + relativeOffset |
||||
val lineBase = textLine.lineBase + relativeOffset |
||||
val lineBottom = textLine.lineBottom + relativeOffset |
||||
drawChars( |
||||
canvas, |
||||
textLine.textChars, |
||||
lineTop, |
||||
lineBase, |
||||
lineBottom, |
||||
textLine.isTitle, |
||||
textLine.isReadAloud |
||||
) |
||||
} |
||||
relativeOffset = relativeOffset(2) |
||||
if (relativeOffset < ChapterProvider.visibleHeight) { |
||||
relativePage(2).textLines.forEach { textLine -> |
||||
val lineTop = textLine.lineTop + relativeOffset |
||||
val lineBase = textLine.lineBase + relativeOffset |
||||
val lineBottom = textLine.lineBottom + relativeOffset |
||||
drawChars( |
||||
canvas, |
||||
textLine.textChars, |
||||
lineTop, |
||||
lineBase, |
||||
lineBottom, |
||||
textLine.isTitle, |
||||
textLine.isReadAloud |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("ClickableViewAccessibility") |
||||
override fun onTouchEvent(event: MotionEvent?): Boolean { |
||||
/** |
||||
* 绘制文字 |
||||
*/ |
||||
private fun drawChars( |
||||
canvas: Canvas, |
||||
textChars: List<TextChar>, |
||||
lineTop: Float, |
||||
lineBase: Float, |
||||
lineBottom: Float, |
||||
isTitle: Boolean, |
||||
isReadAloud: Boolean |
||||
) { |
||||
val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint |
||||
textPaint.color = |
||||
if (isReadAloud) context.accentColor else ReadBookConfig.durConfig.textColor() |
||||
textChars.forEach { |
||||
canvas.drawText(it.charData, it.start, lineBase, textPaint) |
||||
if (it.selected) { |
||||
canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 滚动事件 |
||||
*/ |
||||
fun onScroll(mOffset: Float) { |
||||
if (mOffset == 0f) return |
||||
var offset = mOffset |
||||
if (offset > maxScrollOffset) { |
||||
offset = maxScrollOffset |
||||
} else if (offset < -maxScrollOffset) { |
||||
offset = -maxScrollOffset |
||||
} |
||||
|
||||
pageOffset += offset |
||||
if (pageOffset > 0) { |
||||
if (!pageFactory.hasPrev()) { |
||||
pageOffset = 0f |
||||
} else { |
||||
pageFactory.moveToPrev() |
||||
textPage = pageFactory.currentPage |
||||
pageOffset -= textPage.height |
||||
upView?.invoke(textPage) |
||||
} |
||||
} else if (pageOffset < -textPage.height) { |
||||
if (!pageFactory.hasNext()) { |
||||
pageOffset = -textPage.height.toFloat() |
||||
} else { |
||||
pageOffset += textPage.height |
||||
pageFactory.moveToNext() |
||||
textPage = pageFactory.currentPage |
||||
upView?.invoke(textPage) |
||||
} |
||||
} |
||||
invalidate() |
||||
} |
||||
|
||||
return true |
||||
fun resetPageOffset() { |
||||
pageOffset = 0f |
||||
} |
||||
|
||||
override fun onDraw(canvas: Canvas) { |
||||
super.onDraw(canvas) |
||||
textPage?.let { textPage -> |
||||
textPage.textLines.forEach { textLine -> |
||||
val textPaint = if (textLine.isTitle) { |
||||
ChapterProvider.titlePaint |
||||
} else { |
||||
ChapterProvider.contentPaint |
||||
/** |
||||
* 选择初始文字 |
||||
*/ |
||||
fun selectText( |
||||
x: Float, |
||||
y: Float, |
||||
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit |
||||
) { |
||||
if (!selectAble) return |
||||
if (!visibleRect.contains(x, y)) return |
||||
var relativeOffset = relativeOffset(0) |
||||
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
textChar.selected = true |
||||
invalidate() |
||||
selectStart[0] = 0 |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
selectEnd[0] = 0 |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) |
||||
select(0, lineIndex, charIndex) |
||||
return |
||||
} |
||||
} |
||||
textPaint.color = if (textLine.isReadAloud) { |
||||
context.accentColor |
||||
} else { |
||||
ReadBookConfig.durConfig.textColor() |
||||
return |
||||
} |
||||
} |
||||
if (!ReadBookConfig.isScroll) return |
||||
//滚动翻页 |
||||
relativeOffset = relativeOffset(1) |
||||
if (relativeOffset >= ChapterProvider.visibleHeight) return |
||||
val nextPage = relativePage(1) |
||||
for ((lineIndex, textLine) in nextPage.textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
textChar.selected = true |
||||
invalidate() |
||||
selectStart[0] = 1 |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
selectEnd[0] = 1 |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) |
||||
select(1, lineIndex, charIndex) |
||||
return |
||||
} |
||||
} |
||||
textLine.textChars.forEach { |
||||
canvas.drawText( |
||||
it.charData, |
||||
it.leftBottomPosition.x, |
||||
it.leftBottomPosition.y.toFloat(), |
||||
textPaint |
||||
) |
||||
if (it.selected) { |
||||
canvas.drawRect( |
||||
it.leftBottomPosition.x, |
||||
it.rightTopPosition.y.toFloat(), |
||||
it.rightTopPosition.x, |
||||
it.leftBottomPosition.y.toFloat(), |
||||
selectedPaint |
||||
) |
||||
return |
||||
} |
||||
} |
||||
relativeOffset = relativeOffset(2) |
||||
if (relativeOffset >= ChapterProvider.visibleHeight) return |
||||
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
textChar.selected = true |
||||
invalidate() |
||||
selectStart[0] = 2 |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
selectEnd[0] = 2 |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) |
||||
select(2, lineIndex, charIndex) |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun selectText(x: Float, y: Float): SelectPoint? { |
||||
textPage?.let { textPage -> |
||||
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { |
||||
if (y > textLine.lineTop && y < textLine.lineBottom) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { |
||||
textChar.selected = true |
||||
invalidate() |
||||
selectStartLine = lineIndex |
||||
selectStartChar = charIndex |
||||
selectEndLine = lineIndex |
||||
selectEndChar = charIndex |
||||
return SelectPoint( |
||||
textChar.leftBottomPosition.x, |
||||
textChar.leftBottomPosition.y.toFloat(), |
||||
textChar.rightTopPosition.x, |
||||
textChar.leftBottomPosition.y.toFloat() |
||||
) |
||||
/** |
||||
* 开始选择符移动 |
||||
*/ |
||||
fun selectStartMove(x: Float, y: Float) { |
||||
if (!visibleRect.contains(x, y)) return |
||||
var relativeOffset = relativeOffset(0) |
||||
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
if (selectStart[0] != 0 || selectStart[1] != lineIndex || selectStart[2] != charIndex) { |
||||
if (selectToInt(0, lineIndex, charIndex) > selectToInt(selectEnd)) { |
||||
return |
||||
} |
||||
selectStart[0] = 0 |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) |
||||
upSelectChars() |
||||
} |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
} |
||||
if (!ReadBookConfig.isScroll) return |
||||
//滚动翻页 |
||||
relativeOffset = relativeOffset(1) |
||||
if (relativeOffset >= ChapterProvider.visibleHeight) return |
||||
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
if (selectStart[0] != 1 || selectStart[1] != lineIndex || selectStart[2] != charIndex) { |
||||
if (selectToInt(1, lineIndex, charIndex) > selectToInt(selectEnd)) { |
||||
return |
||||
} |
||||
selectStart[0] = 1 |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) |
||||
upSelectChars() |
||||
} |
||||
return |
||||
} |
||||
break |
||||
} |
||||
return |
||||
} |
||||
} |
||||
relativeOffset = relativeOffset(2) |
||||
if (relativeOffset >= ChapterProvider.visibleHeight) return |
||||
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
if (selectStart[0] != 2 || selectStart[1] != lineIndex || selectStart[2] != charIndex) { |
||||
if (selectToInt(2, lineIndex, charIndex) > selectToInt(selectEnd)) { |
||||
return |
||||
} |
||||
selectStart[0] = 2 |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) |
||||
upSelectChars() |
||||
} |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
fun selectStartMove(x: Float, y: Float) { |
||||
/** |
||||
* 结束选择符移动 |
||||
*/ |
||||
fun selectEndMove(x: Float, y: Float) { |
||||
if (!visibleRect.contains(x, y)) return |
||||
var relativeOffset = relativeOffset(0) |
||||
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
if (selectEnd[0] != 0 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) { |
||||
if (selectToInt(0, lineIndex, charIndex) < selectToInt(selectStart)) { |
||||
return |
||||
} |
||||
selectEnd[0] = 0 |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) |
||||
upSelectChars() |
||||
} |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
} |
||||
if (!ReadBookConfig.isScroll) return |
||||
//滚动翻页 |
||||
relativeOffset = relativeOffset(1) |
||||
if (relativeOffset >= ChapterProvider.visibleHeight) return |
||||
for ((lineIndex, textLine) in relativePage(1).textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
if (selectEnd[0] != 1 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) { |
||||
if (selectToInt(1, lineIndex, charIndex) < selectToInt(selectStart)) { |
||||
return |
||||
} |
||||
selectEnd[0] = 1 |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) |
||||
upSelectChars() |
||||
} |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
} |
||||
relativeOffset = relativeOffset(2) |
||||
if (relativeOffset >= ChapterProvider.visibleHeight) return |
||||
for ((lineIndex, textLine) in relativePage(2).textLines.withIndex()) { |
||||
if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
if (x > textChar.start && x < textChar.end) { |
||||
if (selectEnd[0] != 2 || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) { |
||||
if (selectToInt(2, lineIndex, charIndex) < selectToInt(selectStart)) { |
||||
return |
||||
} |
||||
selectEnd[0] = 2 |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) |
||||
upSelectChars() |
||||
} |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 选择开始文字 |
||||
*/ |
||||
fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { |
||||
selectStart[0] = relativePage |
||||
selectStart[1] = lineIndex |
||||
selectStart[2] = charIndex |
||||
val textLine = relativePage(relativePage).textLines[lineIndex] |
||||
val textChar = textLine.textChars[charIndex] |
||||
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset(relativePage)) |
||||
upSelectChars() |
||||
} |
||||
|
||||
fun selectEndMove(x: Float, y: Float) { |
||||
/** |
||||
* 选择结束文字 |
||||
*/ |
||||
fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { |
||||
selectEnd[0] = relativePage |
||||
selectEnd[1] = lineIndex |
||||
selectEnd[2] = charIndex |
||||
val textLine = relativePage(relativePage).textLines[lineIndex] |
||||
val textChar = textLine.textChars[charIndex] |
||||
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage)) |
||||
upSelectChars() |
||||
} |
||||
|
||||
private fun upSelectChars() { |
||||
val last = if (ReadBookConfig.isScroll) 2 else 0 |
||||
for (relativePos in 0..last) { |
||||
for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { |
||||
for ((charIndex, textChar) in textLine.textChars.withIndex()) { |
||||
textChar.selected = |
||||
if (relativePos == selectStart[0] |
||||
&& relativePos == selectEnd[0] |
||||
&& lineIndex == selectStart[1] |
||||
&& lineIndex == selectEnd[1] |
||||
) { |
||||
charIndex in selectStart[2]..selectEnd[2] |
||||
} else if (relativePos == selectStart[0] && lineIndex == selectStart[1]) { |
||||
charIndex >= selectStart[2] |
||||
} else if (relativePos == selectEnd[0] && lineIndex == selectEnd[1]) { |
||||
charIndex <= selectEnd[2] |
||||
} else if (relativePos == selectStart[0] && relativePos == selectEnd[0]) { |
||||
lineIndex in (selectStart[1] + 1) until selectEnd[1] |
||||
} else if (relativePos == selectStart[0]) { |
||||
lineIndex > selectStart[1] |
||||
} else if (relativePos == selectEnd[0]) { |
||||
lineIndex < selectEnd[1] |
||||
} else { |
||||
relativePos in selectStart[0] + 1 until selectEnd[0] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
invalidate() |
||||
} |
||||
|
||||
private fun upSelectedStart(x: Float, y: Float) { |
||||
callBack.upSelectedStart(x, y + callBack.headerHeight) |
||||
} |
||||
|
||||
private fun upSelectedEnd(x: Float, y: Float) { |
||||
callBack.upSelectedEnd(x, y + callBack.headerHeight) |
||||
} |
||||
|
||||
fun cancelSelect() { |
||||
val last = if (ReadBookConfig.isScroll) 2 else 0 |
||||
for (relativePos in 0..last) { |
||||
relativePage(relativePos).textLines.forEach { textLine -> |
||||
textLine.textChars.forEach { |
||||
it.selected = false |
||||
} |
||||
} |
||||
} |
||||
invalidate() |
||||
callBack.onCancelSelect() |
||||
} |
||||
|
||||
val selectedText: String |
||||
get() { |
||||
val stringBuilder = StringBuilder() |
||||
for (relativePos in selectStart[0]..selectEnd[0]) { |
||||
val textPage = relativePage(relativePos) |
||||
if (relativePos == selectStart[0] && relativePos == selectEnd[0]) { |
||||
for (lineIndex in selectStart[1]..selectEnd[1]) { |
||||
if (lineIndex == selectStart[1] && lineIndex == selectEnd[1]) { |
||||
stringBuilder.append( |
||||
textPage.textLines[lineIndex].text.substring( |
||||
selectStart[2], |
||||
selectEnd[2] + 1 |
||||
) |
||||
) |
||||
} else if (lineIndex == selectStart[1]) { |
||||
stringBuilder.append( |
||||
textPage.textLines[lineIndex].text.substring( |
||||
selectStart[2] |
||||
) |
||||
) |
||||
} else if (lineIndex == selectEnd[1]) { |
||||
stringBuilder.append( |
||||
textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1) |
||||
) |
||||
} else { |
||||
stringBuilder.append(textPage.textLines[lineIndex].text) |
||||
} |
||||
} |
||||
} else if (relativePos == selectStart[0]) { |
||||
for (lineIndex in selectStart[1] until relativePage(relativePos).textLines.size) { |
||||
if (lineIndex == selectStart[1]) { |
||||
stringBuilder.append( |
||||
textPage.textLines[lineIndex].text.substring( |
||||
selectStart[2] |
||||
) |
||||
) |
||||
} else { |
||||
stringBuilder.append(textPage.textLines[lineIndex].text) |
||||
} |
||||
} |
||||
} else if (relativePos == selectEnd[0]) { |
||||
for (lineIndex in 0..selectEnd[1]) { |
||||
if (lineIndex == selectEnd[1]) { |
||||
stringBuilder.append( |
||||
textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1) |
||||
) |
||||
} else { |
||||
stringBuilder.append(textPage.textLines[lineIndex].text) |
||||
} |
||||
} |
||||
} else if (relativePos in selectStart[0] + 1 until selectEnd[0]) { |
||||
for (lineIndex in selectStart[1]..selectEnd[1]) { |
||||
stringBuilder.append(textPage.textLines[lineIndex].text) |
||||
} |
||||
} |
||||
} |
||||
return stringBuilder.toString() |
||||
} |
||||
|
||||
private fun selectToInt(page: Int, line: Int, char: Int): Int { |
||||
return page * 1000000 + line * 100000 + char |
||||
} |
||||
|
||||
private fun selectToInt(select: Array<Int>): Int { |
||||
return select[0] * 1000000 + select[1] * 100000 + select[2] |
||||
} |
||||
|
||||
private fun relativeOffset(relativePos: Int): Float { |
||||
return when (relativePos) { |
||||
0 -> pageOffset |
||||
1 -> pageOffset + textPage.height |
||||
else -> pageOffset + textPage.height + pageFactory.nextPage.height |
||||
} |
||||
} |
||||
|
||||
private fun relativePage(relativePos: Int): TextPage { |
||||
return when (relativePos) { |
||||
0 -> textPage |
||||
1 -> pageFactory.nextPage |
||||
else -> pageFactory.nextPagePlus |
||||
} |
||||
} |
||||
|
||||
interface CallBack { |
||||
fun upSelectedStart(x: Float, y: Float) |
||||
fun upSelectedEnd(x: Float, y: Float) |
||||
fun onCancelSelect() |
||||
val headerHeight: Int |
||||
val pageFactory: TextPageFactory |
||||
} |
||||
} |
||||
|
@ -1,24 +1,19 @@ |
||||
package io.legado.app.ui.book.read.page |
||||
|
||||
import io.legado.app.service.help.ReadBook |
||||
import io.legado.app.ui.book.read.page.entities.TextChapter |
||||
|
||||
interface DataSource { |
||||
val isScrollDelegate: Boolean |
||||
|
||||
val pageIndex: Int |
||||
val pageIndex: Int get() = ReadBook.durChapterPos() |
||||
|
||||
fun setPageIndex(pageIndex: Int) |
||||
val currentChapter: TextChapter? |
||||
|
||||
fun getChapterPosition(): Int |
||||
val nextChapter: TextChapter? |
||||
|
||||
fun getCurrentChapter(): TextChapter? |
||||
|
||||
fun getNextChapter(): TextChapter? |
||||
|
||||
fun getPreviousChapter(): TextChapter? |
||||
val prevChapter: TextChapter? |
||||
|
||||
fun hasNextChapter(): Boolean |
||||
|
||||
fun hasPrevChapter(): Boolean |
||||
|
||||
} |
@ -1,119 +1,61 @@ |
||||
package io.legado.app.ui.book.read.page.delegate |
||||
|
||||
import android.graphics.Canvas |
||||
import android.graphics.Matrix |
||||
import android.view.MotionEvent |
||||
import android.view.VelocityTracker |
||||
import io.legado.app.ui.book.read.page.PageView |
||||
import kotlin.math.abs |
||||
|
||||
class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { |
||||
|
||||
private val bitmapMatrix = Matrix() |
||||
// 滑动追踪的时间 |
||||
private val velocityDuration = 1000 |
||||
//速度追踪器 |
||||
private val mVelocity: VelocityTracker = VelocityTracker.obtain() |
||||
|
||||
override fun onAnimStart() { |
||||
if (!atTop && !atBottom) { |
||||
stopScroll() |
||||
return |
||||
} |
||||
val distanceY: Float |
||||
when (mDirection) { |
||||
Direction.NEXT -> distanceY = |
||||
if (isCancel) { |
||||
var dis = viewHeight - startY + touchY |
||||
if (dis > viewHeight) { |
||||
dis = viewHeight.toFloat() |
||||
} |
||||
viewHeight - dis |
||||
} else { |
||||
-(touchY + (viewHeight - startY)) |
||||
} |
||||
else -> distanceY = |
||||
if (isCancel) { |
||||
-(touchY - startY) |
||||
} else { |
||||
viewHeight - (touchY - startY) |
||||
} |
||||
} |
||||
|
||||
startScroll(0, touchY.toInt(), 0, distanceY.toInt()) |
||||
//惯性滚动 |
||||
fling( |
||||
0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(), |
||||
0, 0, -10 * viewHeight, 10 * viewHeight |
||||
) |
||||
} |
||||
|
||||
override fun onDraw(canvas: Canvas) { |
||||
if (atTop || atBottom) { |
||||
val offsetY = touchY - startY |
||||
|
||||
if ((mDirection == Direction.NEXT && offsetY > 0) |
||||
|| (mDirection == Direction.PREV && offsetY < 0) |
||||
) return |
||||
override fun onScroll() { |
||||
curPage.onScroll(touchY - lastY) |
||||
} |
||||
|
||||
val distanceY = if (offsetY > 0) offsetY - viewHeight else offsetY + viewHeight |
||||
if (atTop && mDirection == Direction.PREV) { |
||||
bitmap?.let { |
||||
bitmapMatrix.setTranslate(0.toFloat(), distanceY) |
||||
canvas.drawBitmap(it, bitmapMatrix, null) |
||||
} |
||||
} else if (atBottom && mDirection == Direction.NEXT) { |
||||
bitmap?.let { |
||||
bitmapMatrix.setTranslate(0.toFloat(), distanceY) |
||||
canvas.drawBitmap(it, bitmapMatrix, null) |
||||
override fun onTouch(event: MotionEvent) { |
||||
when (event.action) { |
||||
MotionEvent.ACTION_DOWN -> { |
||||
setStartPoint(event.x, event.y) |
||||
abort() |
||||
mVelocity.clear() |
||||
} |
||||
MotionEvent.ACTION_MOVE -> { |
||||
if (isTextSelected) { |
||||
selectText(event) |
||||
} else { |
||||
onScroll(event) |
||||
} |
||||
} |
||||
} |
||||
super.onTouch(event) |
||||
} |
||||
|
||||
override fun onAnimStop() { |
||||
if (!isCancel) { |
||||
pageView.fillPage(mDirection) |
||||
} |
||||
} |
||||
|
||||
override fun onScroll( |
||||
e1: MotionEvent, |
||||
e2: MotionEvent, |
||||
distanceX: Float, |
||||
distanceY: Float |
||||
): Boolean { |
||||
if (!isMoved && abs(distanceX) < abs(distanceY)) { |
||||
if (distanceY < 0) { |
||||
if (atTop) { |
||||
val event = e1.toAction(MotionEvent.ACTION_UP) |
||||
curPage.dispatchTouchEvent(event) |
||||
event.recycle() |
||||
//如果上一页不存在 |
||||
if (!hasPrev()) { |
||||
noNext = true |
||||
return true |
||||
} |
||||
setDirection(Direction.PREV) |
||||
setBitmap() |
||||
} |
||||
} else { |
||||
if (atBottom) { |
||||
val event = e1.toAction(MotionEvent.ACTION_UP) |
||||
curPage.dispatchTouchEvent(event) |
||||
event.recycle() |
||||
//如果不存在表示没有下一页了 |
||||
if (!hasNext()) { |
||||
noNext = true |
||||
return true |
||||
} |
||||
setDirection(Direction.NEXT) |
||||
setBitmap() |
||||
} |
||||
} |
||||
isMoved = true |
||||
} |
||||
if ((atTop && mDirection != Direction.PREV) || (atBottom && mDirection != Direction.NEXT) || mDirection == Direction.NONE) { |
||||
//传递触摸事件到textView |
||||
curPage.dispatchTouchEvent(e2) |
||||
private fun onScroll(event: MotionEvent) { |
||||
mVelocity.addMovement(event) |
||||
mVelocity.computeCurrentVelocity(velocityDuration) |
||||
setTouchPoint(event.x, event.y) |
||||
if (!isMoved) { |
||||
isMoved = abs(startX - event.x) > slop || abs(startY - event.y) > slop |
||||
} |
||||
if (isMoved) { |
||||
isCancel = if (mDirection == Direction.NEXT) distanceY < 0 else distanceY > 0 |
||||
isRunning = true |
||||
//设置触摸点 |
||||
setTouchPoint(e2.x, e2.y) |
||||
} |
||||
return isMoved |
||||
} |
||||
|
||||
override fun onDestroy() { |
||||
super.onDestroy() |
||||
mVelocity.recycle() |
||||
} |
||||
} |
@ -1,8 +0,0 @@ |
||||
package io.legado.app.ui.book.read.page.entities |
||||
|
||||
data class SelectPoint( |
||||
var startX: Float, |
||||
var startY: Float, |
||||
var endX: Float, |
||||
var endY: Float |
||||
) |
@ -1,6 +0,0 @@ |
||||
package io.legado.app.ui.book.read.page.entities |
||||
|
||||
data class TextPoint( |
||||
val x: Float, |
||||
val y: Int |
||||
) |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue