From fa64ca6b1fb52177a9280f326df9b7b9e51512f7 Mon Sep 17 00:00:00 2001 From: Z2549 <84760771+Z2549@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:47:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=8B=E6=9F=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加手柄方向键及左摇杆翻页的支持 --- .../app/ui/book/read/ReadBookActivity.kt | 3957 +++++++++-------- 1 file changed, 1991 insertions(+), 1966 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 0a1c97a00..2cbd8781d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -1,1966 +1,1991 @@ -package io.legado.app.ui.book.read - -//import io.legado.app.lib.theme.accentColor -import android.annotation.SuppressLint -import android.content.Intent -import android.content.res.Configuration -import android.graphics.Color -import android.os.Bundle -import android.view.Gravity -import android.view.HapticFeedbackConstants -import android.view.InputDevice -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuItem -import android.view.MotionEvent -import android.view.View -import androidx.activity.addCallback -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.PopupMenu -import androidx.core.view.get -import androidx.core.view.size -import androidx.lifecycle.lifecycleScope -import com.google.android.material.transition.platform.MaterialContainerTransform -import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback -import com.jaredrummler.android.colorpicker.ColorPickerDialogListener -import com.script.rhino.runScriptWithContext -import io.legado.app.BuildConfig -import io.legado.app.R -import io.legado.app.constant.AppLog -import io.legado.app.constant.BookType -import io.legado.app.constant.EventBus -import io.legado.app.constant.PreferKey -import io.legado.app.constant.Status -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.BookProgress -import io.legado.app.data.entities.BookSource -import io.legado.app.exception.NoStackTraceException -import io.legado.app.help.AppWebDav -import io.legado.app.help.IntentData -import io.legado.app.help.TTS -import io.legado.app.help.book.BookHelp -import io.legado.app.help.book.ContentProcessor -import io.legado.app.help.book.isAudio -import io.legado.app.help.book.isEpub -import io.legado.app.help.book.isLocal -import io.legado.app.help.book.isLocalTxt -import io.legado.app.help.book.isMobi -import io.legado.app.help.book.removeType -import io.legado.app.help.book.update -import io.legado.app.help.config.AppConfig -import io.legado.app.help.config.ReadBookConfig -import io.legado.app.help.config.ReadTipConfig -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.help.source.getSourceType -import io.legado.app.help.storage.Backup -import io.legado.app.lib.dialogs.SelectItem -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.selector -import io.legado.app.model.ReadAloud -import io.legado.app.model.ReadBook -import io.legado.app.model.analyzeRule.AnalyzeRule -import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter -import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext -import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.paramPattern -import io.legado.app.model.localBook.EpubFile -import io.legado.app.model.localBook.MobiFile -import io.legado.app.receiver.NetworkChangedListener -import io.legado.app.receiver.TimeBatteryReceiver -import io.legado.app.service.BaseReadAloudService -import io.legado.app.ui.about.AppLogDialog -import io.legado.app.ui.book.bookmark.BookmarkDialog -import io.legado.app.ui.book.changesource.ChangeBookSourceDialog -import io.legado.app.ui.book.changesource.ChangeChapterSourceDialog -import io.legado.app.ui.book.info.BookInfoActivity -import io.legado.app.ui.book.read.config.AutoReadDialog -import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR -import io.legado.app.ui.book.read.config.FontConfigDialog -import io.legado.app.ui.book.read.config.FontConfigDialog.Companion.S_COLOR -import io.legado.app.ui.book.read.config.FontConfigDialog.Companion.TEXT_ACCENT_COLOR -import io.legado.app.ui.book.read.config.FontConfigDialog.Companion.TEXT_COLOR -import io.legado.app.ui.book.read.config.FontSelectDialog -import io.legado.app.ui.book.read.config.ReadAloudDialog -import io.legado.app.ui.book.read.config.ReadStyleDialog -import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.A_COLOR -import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.B_COLOR -import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR -import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR -import io.legado.app.ui.book.read.config.ToolButtonConfigDialog -import io.legado.app.ui.book.read.config.UnderlineConfigDialog.Companion.U_COLOR -import io.legado.app.ui.book.read.page.ContentTextView -import io.legado.app.ui.book.read.page.ReadView -import io.legado.app.ui.book.read.page.entities.PageDirection -import io.legado.app.ui.book.read.page.entities.TextPage -import io.legado.app.ui.book.read.page.provider.ChapterProvider -import io.legado.app.ui.book.read.page.provider.LayoutProgressListener -import io.legado.app.ui.book.searchContent.SearchContentActivity -import io.legado.app.ui.book.searchContent.SearchResult -import io.legado.app.ui.book.source.edit.BookSourceEditActivity -import io.legado.app.ui.book.toc.TocActivityResult -import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog -import io.legado.app.ui.browser.WebViewActivity -import io.legado.app.ui.dict.DictDialog -import io.legado.app.ui.login.SourceLoginActivity -import io.legado.app.ui.login.SourceLoginJsExtensions -import io.legado.app.ui.replace.ReplaceEditRoute -import io.legado.app.ui.replace.ReplaceRuleActivity -import io.legado.app.ui.widget.PopupAction -import io.legado.app.ui.widget.dialog.PhotoDialog -import io.legado.app.utils.Debounce -import io.legado.app.utils.GSON -import io.legado.app.utils.LogUtils -import io.legado.app.utils.NetworkUtils -import io.legado.app.utils.StartActivityContract -import io.legado.app.utils.applyOpenTint -import io.legado.app.utils.buildMainHandler -import io.legado.app.utils.dismissDialogFragment -import io.legado.app.utils.fromJsonObject -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.getPrefString -import io.legado.app.utils.hexString -import io.legado.app.utils.iconItemOnLongClick -import io.legado.app.utils.invisible -import io.legado.app.utils.isAbsUrl -import io.legado.app.utils.isTrue -import io.legado.app.utils.navigationBarGravity -import io.legado.app.utils.observeEvent -import io.legado.app.utils.observeEventSticky -import io.legado.app.utils.postEvent -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.showHelp -import io.legado.app.utils.startActivity -import io.legado.app.utils.startActivityForBook -import io.legado.app.utils.sysScreenOffTime -import io.legado.app.utils.themeColor -import io.legado.app.utils.throttle -import io.legado.app.utils.toastOnUi -import io.legado.app.utils.visible -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * 阅读界面 - */ -class ReadBookActivity : BaseReadBookActivity(), - View.OnTouchListener, - ReadView.CallBack, - TextActionMenu.CallBack, - ContentTextView.CallBack, - PopupMenu.OnMenuItemClickListener, - ReadMenu.CallBack, - SearchMenu.CallBack, - ReadAloudDialog.CallBack, - ChangeBookSourceDialog.CallBack, - ChangeChapterSourceDialog.CallBack, - ReadBook.CallBack, - AutoReadDialog.CallBack, - ToolButtonConfigDialog.CallBack, - TxtTocRuleDialog.CallBack, - ColorPickerDialogListener, - FontConfigDialog.CallBack, - FontSelectDialog.CallBack, - LayoutProgressListener { - - private val tocActivity = - registerForActivityResult(TocActivityResult()) { - it?.let { - viewModel.openChapter(it.first, it.second) - } - } - private val sourceEditActivity = - registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) { - if (it.resultCode == RESULT_OK) { - viewModel.upBookSource { - upMenuView() - } - } - } - private val replaceActivity = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - viewModel.replaceRuleChanged() - } - } - - private val searchContentActivity = - registerForActivityResult(StartActivityContract(SearchContentActivity::class.java)) { - val data = it.data ?: return@registerForActivityResult - val key = data.getLongExtra("key", System.currentTimeMillis()) - val index = data.getIntExtra("index", 0) - val searchResult = IntentData.get("searchResult$key") - val searchResultList = IntentData.get>("searchResultList$key") - if (searchResult != null && searchResultList != null) { - viewModel.searchContentQuery = searchResult.query - binding.searchMenu.upSearchResultList(searchResultList) - isShowingSearchResult = true - viewModel.searchResultIndex = index - binding.searchMenu.updateSearchResultIndex(index) - binding.searchMenu.selectedSearchResult?.let { currentResult -> - ReadBook.saveCurrentBookProgress() //退出全文搜索恢复此时进度 - skipToSearch(currentResult) - showActionMenu() - } - } - } - private val bookInfoActivity = - registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) { - if (it.resultCode == RESULT_OK) { - setResult(RESULT_DELETED) - finish() - } else { - ReadBook.loadOrUpContent() - } - } - private var menu: Menu? = null - private var backupJob: Job? = null - private var tts: TTS? = null - val textActionMenu: TextActionMenu by lazy { - TextActionMenu(this, this) - } - private val popupAction: PopupAction by lazy { - PopupAction(this) - } - - // 当前使用的字体路径(给 FontDialog 用) - override val curFontPath: String - get() = ReadBookConfig.textFont - - // 当选择了字体时被调用 - override fun selectFont(path: String) { - // path 为空表示恢复系统默认字体 - ReadBookConfig.textFont = path - // 通知阅读界面刷新字体 - postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5, 2)) - } - - override val isInitFinish: Boolean get() = viewModel.isInitFinish - override val isScroll: Boolean get() = binding.readView.isScroll - private val isAutoPage get() = binding.readView.isAutoPage - override var isShowingSearchResult = false - override var isSelectingSearchResult = false - set(value) { - field = value && isShowingSearchResult - } - private val timeBatteryReceiver = TimeBatteryReceiver() - private var screenTimeOut: Long = 0 - private var loadStates: Boolean = false - override val pageFactory get() = binding.readView.pageFactory - override val pageDelegate get() = binding.readView.pageDelegate - override val headerHeight: Int get() = binding.readView.curPage.headerHeight - override val imgBgPaddingStart: Int get() = binding.readView.curPage.imgBgPaddingStart - private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } } - private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } } - private var bookChanged = false - private var pageChanged = false - private val handler by lazy { buildMainHandler() } - private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } } - private val executor = ReadBook.executor - private val upSeekBarThrottle = throttle(200) { - runOnUiThread { - upSeekBarProgress() - binding.readMenu.upSeekBar() - } - } - - //恢复跳转前进度对话框的交互结果 - private var confirmRestoreProcess: Boolean? = null - private val networkChangedListener by lazy { - NetworkChangedListener(this) - } - private var justInitData: Boolean = false - private var syncDialog: AlertDialog? = null - - private val delayMillis: Long = 500 - private var hasLoadedBook = false - - private fun ensureLoadBook() { - if (!hasLoadedBook) { - hasLoadedBook = true - loadBook() - } - } - - @SuppressLint("ClickableViewAccessibility") - override fun onCreate(savedInstanceState: Bundle?) { - setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) - setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) - val transform = MaterialContainerTransform().apply { - addTarget(binding.root) - scrimColor = Color.TRANSPARENT - duration = delayMillis - } - window.sharedElementEnterTransition = transform - window.sharedElementReturnTransition = transform - - super.onCreate(savedInstanceState) - - binding.rootView.transitionName = intent.getStringExtra("transitionName") - - upScreenTimeOut() - ReadBook.register(this) - binding.readMenu.colorSurfaceContainer = - themeColor(com.google.android.material.R.attr.colorSurfaceContainer) - binding.readMenu.colorSecondary = - themeColor(androidx.appcompat.R.attr.colorPrimary) - binding.readMenu.colorSecondaryContainer = - themeColor(com.google.android.material.R.attr.colorSecondaryContainer) - binding.cursorLeft.setOnTouchListener(this) - binding.cursorRight.setOnTouchListener(this) - - onBackPressedDispatcher.addCallback(this) { - if (isShowingSearchResult) { - exitSearchMenu() - restoreLastBookProcess() - return@addCallback - } - //拦截返回供恢复阅读进度 - if (ReadBook.lastBookProgress != null && confirmRestoreProcess != false) { - restoreLastBookProcess() - return@addCallback - } - if (BaseReadAloudService.isPlay()) { - ReadAloud.pause(this@ReadBookActivity) - toastOnUi(R.string.read_aloud_pause) - return@addCallback - } - if (isAutoPage) { - autoPageStop() - return@addCallback - } - if (getPrefBoolean("disableReturnKey") && !menuLayoutIsVisible) { - return@addCallback - } - if (savedInstanceState != null || !ReadBook.inBookshelf) { - ReadBook.commitReadSession() - finish() - } else { - ReadBook.commitReadSession() - supportFinishAfterTransition() - } - //TODO: 有关测量相关问题 - //孩子们,我的水平不够,只能这样修复测量问题了 - } - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - if (!AppConfig.sharedElementEnterTransitionEnable || !AppConfig.delayBookLoadEnable) { - ensureLoadBook() - } - //虽然很扯但有用 - binding.rootView.postDelayed({ - ensureLoadBook() - }, delayMillis) - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - viewModel.initData(intent) - } - - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - upSystemUiVisibility() - if (hasFocus) { - binding.readMenu.upBrightnessState() - } else if (!menuLayoutIsVisible) { - ReadBook.cancelPreDownloadTask() - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - //upSystemUiVisibility() - binding.readView.upStyle() - recreate() - } - - - override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) { - if (!isTopResumedActivity) { - ReadBook.cancelPreDownloadTask() - } - } - - @SuppressLint("UnspecifiedRegisterReceiverFlag") - override fun onResume() { - super.onResume() - ReadBook.readStartTime = System.currentTimeMillis() - ReadBook.initReadTime() - ReadBook.startAutoSaveSession() - if (bookChanged) { - bookChanged = false - ReadBook.callBack = this - viewModel.initData(intent) - justInitData = true - } else { - //web端阅读时,app处于阅读界面,本地记录会覆盖web保存的进度,在此处恢复 - ReadBook.webBookProgress?.let { - ReadBook.setProgress(it) - ReadBook.webBookProgress = null - } - } - upSystemUiVisibility() - registerReceiver(timeBatteryReceiver, timeBatteryReceiver.filter) - binding.readView.upTime() - screenOffTimerStart() - // 网络监听,当从无网切换到网络环境时同步进度(注意注册的同时就会收到监听,因此界面激活时无需重复执行同步操作) - networkChangedListener.register() - networkChangedListener.onNetworkChanged = { - // 当网络是可用状态且无需初始化时同步进度(初始化中已有同步进度逻辑) - if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData) { - ReadBook.syncProgress({ progress -> sureNewProgress(progress) }) - } - } - } - - override fun onPause() { - super.onPause() - autoPageStop() - backupJob?.cancel() - ReadBook.saveRead() - ReadBook.stopAutoSaveSession() - ReadBook.commitReadSession() - ReadBook.cancelPreDownloadTask() - unregisterReceiver(timeBatteryReceiver) - upSystemUiVisibility() - if (!BuildConfig.DEBUG) { - if (AppConfig.syncBookProgressPlus) { - ReadBook.syncProgress() - } else { - ReadBook.uploadProgress() - } - Backup.autoBack(this) - } - justInitData = false - networkChangedListener.unRegister() - } - - override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.book_read, menu) - menu.iconItemOnLongClick(R.id.menu_change_source) { - PopupMenu(this, it).apply { - inflate(R.menu.book_read_change_source) - this.menu.applyOpenTint(this@ReadBookActivity) - setOnMenuItemClickListener(this@ReadBookActivity) - }.show() - } - menu.iconItemOnLongClick(R.id.menu_refresh) { - PopupMenu(this, it).apply { - inflate(R.menu.book_read_refresh) - this.menu.applyOpenTint(this@ReadBookActivity) - setOnMenuItemClickListener(this@ReadBookActivity) - }.show() - } - binding.readMenu.refreshMenuColorFilter() - return super.onCompatCreateOptionsMenu(menu) - } - - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - this.menu = menu - upMenu() - return super.onPrepareOptionsMenu(menu) - } - - override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { - menu.findItem(R.id.menu_same_title_removed)?.isChecked = - ReadBook.curTextChapter?.sameTitleRemoved == true - return super.onMenuOpened(featureId, menu) - } - - private fun loadBook() { - lifecycleScope.launch(Dispatchers.Default) { - viewModel.initReadBookConfig(intent) - viewModel.initData(intent) - withContext(Main) { - justInitData = true - } - } - } - - /** - * 更新菜单 - */ - private fun upMenu() { - val menu = menu ?: return - val book = ReadBook.book ?: return - val onLine = !book.isLocal - for (i in 0 until menu.size) { - val item = menu[i] - when (item.groupId) { - R.id.menu_group_on_line -> item.isVisible = onLine - R.id.menu_group_local -> item.isVisible = !onLine - R.id.menu_group_text -> item.isVisible = book.isLocalTxt - R.id.menu_group_epub -> item.isVisible = book.isEpub - else -> when (item.itemId) { - R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() - R.id.menu_re_segment -> item.isChecked = book.getReSegment() -// R.id.menu_enable_review -> { -// item.isVisible = BuildConfig.DEBUG -// item.isChecked = AppConfig.enableReview -// } - - R.id.menu_reverse_content -> item.isVisible = onLine - R.id.menu_del_ruby_tag -> item.isChecked = book.getDelTag(Book.rubyTag) - R.id.menu_del_h_tag -> item.isChecked = book.getDelTag(Book.hTag) - } - } - } - lifecycleScope.launch { - menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) { - AppWebDav.isOk - } - menu.findItem(R.id.menu_cover_progress)?.isVisible = withContext(IO) { - AppWebDav.isOk - } - } - } - - private fun defaultChangeSource() { - if (AppConfig.defaultSourceChangeAll) - { - binding.readMenu.runMenuOut() - ReadBook.book?.let { - showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) - } - } else { - lifecycleScope.launch { - val book = ReadBook.book ?: return@launch - val chapter = - appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) - ?: return@launch - binding.readMenu.runMenuOut() - showDialogFragment( - ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title) - ) - } - } - } - - /** - * 菜单 - */ - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_change_source -> { defaultChangeSource() } - - R.id.menu_book_change_source -> { - binding.readMenu.runMenuOut() - ReadBook.book?.let { - showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) - } - } - - R.id.menu_chapter_change_source -> lifecycleScope.launch { - val book = ReadBook.book ?: return@launch - val chapter = - appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) - ?: return@launch - binding.readMenu.runMenuOut() - showDialogFragment( - ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title) - ) - } - - R.id.menu_refresh, - R.id.menu_refresh_dur -> { - if (ReadBook.bookSource == null) { - upContent() - } else { - ReadBook.book?.let { - ReadBook.curTextChapter = null - binding.readView.upContent() - viewModel.refreshContentDur(it) - } - } - } - - R.id.menu_refresh_after -> { - if (ReadBook.bookSource == null) { - upContent() - } else { - ReadBook.book?.let { - ReadBook.clearTextChapter() - binding.readView.upContent() - viewModel.refreshContentAfter(it) - } - } - } - - R.id.menu_refresh_all -> { - if (ReadBook.bookSource == null) { - upContent() - } else { - ReadBook.book?.let { - refreshContentAll(it) - } - } - } - - R.id.menu_setting_replace -> openReplaceRule() - R.id.menu_download -> showDownloadDialog() - R.id.menu_add_bookmark -> addBookmark() - R.id.menu_simulated_reading -> showSimulatedReading() - R.id.menu_edit_content -> { - binding.readMenu.runMenuOut() - showDialogFragment(ContentEditDialog()) - } - R.id.menu_update_toc -> ReadBook.book?.let { - if (it.isEpub) { - BookHelp.clearCache(it) - EpubFile.clear() - } - if (it.isMobi) { - MobiFile.clear() - } - loadChapterList(it) - } - R.id.menu_enable_replace -> { - changeReplaceRuleState() - } - R.id.menu_re_segment -> ReadBook.book?.let { - it.setReSegment(!it.getReSegment()) - item.isChecked = it.getReSegment() - ReadBook.loadContent(false) - } - - R.id.menu_tool_button -> { - binding.readMenu.runMenuOut() - showDialogFragment(ToolButtonConfigDialog()) - } - -// R.id.menu_enable_review -> { -// AppConfig.enableReview = !AppConfig.enableReview -// item.isChecked = AppConfig.enableReview -// ReadBook.loadContent(false) -// } - - R.id.menu_del_ruby_tag -> ReadBook.book?.let { - item.isChecked = !item.isChecked - if (item.isChecked) { - it.addDelTag(Book.rubyTag) - } else { - it.removeDelTag(Book.rubyTag) - } - refreshContentAll(it) - } - - R.id.menu_del_h_tag -> ReadBook.book?.let { - item.isChecked = !item.isChecked - if (item.isChecked) { - it.addDelTag(Book.hTag) - } else { - it.removeDelTag(Book.hTag) - } - refreshContentAll(it) - } - - R.id.menu_page_anim -> showPageAnimConfig { - binding.readView.upPageAnim() - ReadBook.loadContent(false) - } - - R.id.menu_log -> showDialogFragment() - R.id.menu_toc_regex -> showDialogFragment( - TxtTocRuleDialog(ReadBook.book?.tocUrl) - ) - - R.id.menu_reverse_content -> ReadBook.book?.let { - viewModel.reverseContent(it) - } - - R.id.menu_set_charset -> showCharsetConfig() - R.id.menu_image_style -> { - val imgStyles = - arrayListOf( - Book.imgStyleDefault, Book.imgStyleFull, Book.imgStyleText, - Book.imgStyleSingle - ) - selector( - R.string.image_style, - imgStyles - ) { _, index -> - val imageStyle = imgStyles[index] - ReadBook.book?.setImageStyle(imageStyle) - if (imageStyle == Book.imgStyleSingle) { - ReadBook.book?.setPageAnim(0) // 切换图片样式single后,自动切换为覆盖 - binding.readView.upPageAnim() - } - ReadBook.loadContent(false) - } - } - - R.id.menu_get_progress -> ReadBook.book?.let { - viewModel.syncBookProgress(it) { progress -> - sureSyncProgress(progress) - } - } - - R.id.menu_cover_progress -> ReadBook.book?.let { - ReadBook.uploadProgress(true) { toastOnUi(R.string.upload_book_success) } - } - - R.id.menu_same_title_removed -> { - ReadBook.book?.let { - val contentProcessor = ContentProcessor.get(it) - val textChapter = ReadBook.curTextChapter - if (textChapter != null - && !textChapter.sameTitleRemoved - && !contentProcessor.removeSameTitleCache.contains( - textChapter.chapter.getFileName("nr") - ) - ) { - toastOnUi("未找到可移除的重复标题") - } - } - viewModel.reverseRemoveSameTitle() - } - - R.id.menu_effective_replaces -> showDialogFragment() - - R.id.menu_help -> showHelp() - } - return super.onCompatOptionsItemSelected(item) - } - - private fun refreshContentAll(book: Book) { - ReadBook.clearTextChapter() - binding.readView.upContent() - viewModel.refreshContentAll(book) - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - return onCompatOptionsItemSelected(item) - } - - /** - * 按键拦截,显示菜单 - */ - override fun dispatchKeyEvent(event: KeyEvent): Boolean { - val keyCode = event.keyCode - val action = event.action - val isDown = action == 0 - - if (keyCode == KeyEvent.KEYCODE_MENU) { - if (isDown && !binding.readMenu.canShowMenu) { - binding.readMenu.runMenuIn() - return true - } - if (!isDown && !binding.readMenu.canShowMenu) { - binding.readMenu.canShowMenu = true - return true - } - } - return super.dispatchKeyEvent(event) - } - - /** - * 鼠标滚轮事件 - */ - override fun onGenericMotionEvent(event: MotionEvent): Boolean { - if (0 != (event.source and InputDevice.SOURCE_CLASS_POINTER)) { - if (event.action == MotionEvent.ACTION_SCROLL) { - val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) - LogUtils.d("onGenericMotionEvent", "axisValue = $axisValue") - // 获得垂直坐标上的滚动方向 - if (axisValue < 0.0f) { // 滚轮向下滚 - mouseWheelPage(PageDirection.NEXT) - } else { // 滚轮向上滚 - mouseWheelPage(PageDirection.PREV) - } - return true - } - } - return super.onGenericMotionEvent(event) - } - - /** - * 按键事件 - */ - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (menuLayoutIsVisible) { - return super.onKeyDown(keyCode, event) - } - val longPress = event.repeatCount > 0 - when { - isPrevKey(keyCode) -> { - handleKeyPage(PageDirection.PREV, longPress) - return true - } - - isNextKey(keyCode) -> { - handleKeyPage(PageDirection.NEXT, longPress) - return true - } - } - when (keyCode) { - KeyEvent.KEYCODE_VOLUME_UP -> if (volumeKeyPage(PageDirection.PREV, longPress)) { - return true - } - - KeyEvent.KEYCODE_VOLUME_DOWN -> if (volumeKeyPage(PageDirection.NEXT, longPress)) { - return true - } - - KeyEvent.KEYCODE_PAGE_UP -> { - handleKeyPage(PageDirection.PREV, longPress) - return true - } - - KeyEvent.KEYCODE_PAGE_DOWN -> { - handleKeyPage(PageDirection.NEXT, longPress) - return true - } - - KeyEvent.KEYCODE_SPACE -> { - handleKeyPage(PageDirection.NEXT, longPress) - return true - } - } - - return super.onKeyDown(keyCode, event) - } - - /** - * 松开按键事件 - */ - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - when (keyCode) { - KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (volumeKeyPage(PageDirection.NONE, false)) { - return true - } - } - - } - return super.onKeyUp(keyCode, event) - } - - /** - * view触摸,文字选择 - */ - @SuppressLint("ClickableViewAccessibility") - override fun onTouch(v: View, event: MotionEvent): Boolean = binding.run { - if (!binding.readView.isTextSelected) { - return false - } - when (event.action) { - MotionEvent.ACTION_DOWN -> textActionMenu.dismiss() - MotionEvent.ACTION_MOVE -> { - when (v.id) { - R.id.cursor_left -> if (!readView.curPage.getReverseStartCursor()) { - readView.curPage.selectStartMove( - event.rawX + cursorLeft.width, - event.rawY - cursorLeft.height - ) - } else { - readView.curPage.selectEndMove( - event.rawX - cursorRight.width, - event.rawY - cursorRight.height - ) - } - - R.id.cursor_right -> if (readView.curPage.getReverseEndCursor()) { - readView.curPage.selectStartMove( - event.rawX + cursorLeft.width, - event.rawY - cursorLeft.height - ) - } else { - readView.curPage.selectEndMove( - event.rawX - cursorRight.width, - event.rawY - cursorRight.height - ) - } - } - } - - MotionEvent.ACTION_UP -> { - readView.curPage.resetReverseCursor() - showTextActionMenu() - } - } - return true - } - - /** - * 更新文字选择开始位置 - */ - override fun upSelectedStart(x: Float, y: Float, top: Float): Unit = binding.run { - cursorLeft.x = x - cursorLeft.width - cursorLeft.y = y - cursorLeft.visible(true) - textMenuPosition.x = x - textMenuPosition.y = top - - root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - } - - /** - * 更新文字选择结束位置 - */ - override fun upSelectedEnd(x: Float, y: Float): Unit = binding.run { - cursorRight.x = x - cursorRight.y = y - cursorRight.visible(true) - - root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - } - - /** - * 取消文字选择 - */ - override fun onCancelSelect() = binding.run { - cursorLeft.invisible() - cursorRight.invisible() - textActionMenu.dismiss() - } - - override fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean { - return binding.readView.onTouchEvent(event) - } - - /** - * 显示文本操作菜单 - */ - override fun showTextActionMenu() { - val navigationBarHeight = - if (!ReadBookConfig.hideNavigationBar && navigationBarGravity == Gravity.BOTTOM) - binding.navigationBar.height else 0 - textActionMenu.show( - binding.textMenuPosition, - binding.root.height + navigationBarHeight, - binding.textMenuPosition.x.toInt(), - binding.textMenuPosition.y.toInt(), - binding.cursorLeft.y.toInt() + binding.cursorLeft.height, - binding.cursorRight.x.toInt(), - binding.cursorRight.y.toInt() + binding.cursorRight.height - ) - } - - /** - * 当前选择的文本 - */ - override val selectedText: String get() = binding.readView.getSelectText() - - /** - * 文本选择菜单操作 - */ - override fun onMenuItemSelected(itemId: Int): Boolean { - when (itemId) { - R.id.menu_aloud -> when (AppConfig.contentSelectSpeakMod) { - 1 -> lifecycleScope.launch { - binding.readView.aloudStartSelect() - } - - else -> speak(binding.readView.getSelectText()) - } - - R.id.menu_bookmark -> binding.readView.curPage.let { - val bookmark = it.createBookmark() - if (bookmark == null) { - toastOnUi(R.string.create_bookmark_error) - } else { - showDialogFragment(BookmarkDialog(bookmark)) - } - return true - } - - R.id.menu_edit -> { - val startPos = ReadBook.durChapterPos - showDialogFragment { - putInt("start_position", startPos) - putString("selected_text", binding.readView.getSelectText()) - } - return true - } - - R.id.menu_replace -> { - val scopes = arrayListOf() - ReadBook.book?.name?.let { scopes.add(it) } - ReadBook.bookSource?.bookSourceUrl?.let { scopes.add(it) } - - val text = selectedText.lineSequence().map { it.trim() }.joinToString("\n") - - val editRoute = ReplaceEditRoute( - id = -1, - pattern = text, - scope = scopes.joinToString(";"), - isScopeTitle = false, - isScopeContent = true - ) - - replaceActivity.launch( - ReplaceRuleActivity.startIntent( - context = this, - editRoute = editRoute - ) - ) - return true - } - - R.id.menu_search_content -> { - viewModel.searchContentQuery = selectedText - openSearchActivity(selectedText) - return true - } - - R.id.menu_dict -> { - showDialogFragment(DictDialog(selectedText)) - return true - } - } - return false - } - - private fun onEditTextAction(selectedText: String, startPos: Int) { - val bundle = Bundle().apply { - putString("selected_text", selectedText) - putInt("start_position", startPos) - } - - val dialog = ContentEditDialog().apply { - arguments = bundle - } - - dialog.show(supportFragmentManager, "ContentEditDialog") - } - - /** - * 文本选择菜单操作完成 - */ - override fun onMenuActionFinally() = binding.run { - textActionMenu.dismiss() - readView.cancelSelect() - } - - private fun speak(text: String) { - if (tts == null) { - tts = TTS() - } - tts?.speak(text) - } - - /** - * 鼠标滚轮翻页 - */ - private fun mouseWheelPage(direction: PageDirection) { - if (menuLayoutIsVisible || !AppConfig.mouseWheelPage) { - return - } - keyPageDebounce(direction, mouseWheel = true, longPress = false) - } - - /** - * 音量键翻页 - */ - private fun volumeKeyPage(direction: PageDirection, longPress: Boolean): Boolean { - if (!AppConfig.volumeKeyPage) { - return false - } - if (!AppConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) { - return false - } - handleKeyPage(direction, longPress) - return true - } - - private fun handleKeyPage(direction: PageDirection, longPress: Boolean) { - if (AppConfig.keyPageOnLongPress || direction == PageDirection.NONE) { - keyPage(direction) - } else { - keyPageDebounce(direction, longPress = longPress) - } - } - - private fun keyPageDebounce( - direction: PageDirection, - mouseWheel: Boolean = false, - longPress: Boolean - ) { - if (longPress) { - return - } - nextPageDebounce.apply { - wait = if (mouseWheel) 200L else 600L - leading = !mouseWheel - trailing = mouseWheel - } - prevPageDebounce.apply { - wait = if (mouseWheel) 200L else 600L - leading = !mouseWheel - trailing = mouseWheel - } - when (direction) { - PageDirection.NEXT -> nextPageDebounce.invoke() - PageDirection.PREV -> prevPageDebounce.invoke() - else -> {} - } - } - - private fun keyPage(direction: PageDirection) { - binding.readView.cancelSelect() - binding.readView.pageDelegate?.isCancel = false - binding.readView.pageDelegate?.keyTurnPage(direction) - } - - override fun upMenuView() { - handler.post { - upMenu() - binding.readMenu.upBookView() - } - } - - override fun loadChapterList(book: Book) { - ReadBook.upMsg(getString(R.string.toc_updateing)) - viewModel.loadChapterList(book) - } - - /** - * 内容加载完成 - */ - override fun contentLoadFinish() { - if (intent.getBooleanExtra("readAloud", false)) { - intent.removeExtra("readAloud") - ReadBook.readAloud() - } - loadStates = true - } - - /** - * 更新内容 - */ - override fun upContent( - relativePosition: Int, - resetPageOffset: Boolean, - success: (() -> Unit)? - ) { - lifecycleScope.launch { - binding.readView.upContent(relativePosition, resetPageOffset) - if (relativePosition == 0) { - upSeekBarProgress() - } - loadStates = false - success?.invoke() - } - } - - override suspend fun upContentAwait( - relativePosition: Int, - resetPageOffset: Boolean, - success: (() -> Unit)? - ) = withContext(Main.immediate) { - binding.readView.upContent(relativePosition, resetPageOffset) - if (relativePosition == 0) { - upSeekBarProgress() - } - loadStates = false - } - - override fun upPageAnim(upRecorder: Boolean) { - lifecycleScope.launch { - binding.readView.upPageAnim(upRecorder) - } - } - - override fun notifyBookChanged() { - bookChanged = true - if (!ReadBook.inBookshelf) { - viewModel.removeFromBookshelf { super.finish() } - } - } - - override fun cancelSelect() { - runOnUiThread { - binding.readView.cancelSelect() - } - } - - /** - * 页面改变 - */ - override fun pageChanged() { - pageChanged = true - binding.readView.onPageChange() - handler.post { - upSeekBarProgress() - } - executor.execute { - startBackupJob() - } - } - - /** - * 更新进度条位置 - */ - private fun upSeekBarProgress() { - val progress = when (AppConfig.progressBarBehavior) { - "page" -> ReadBook.durPageIndex - else /* chapter */ -> ReadBook.durChapterIndex - } - if (progress >= 0) { - binding.readMenu.setSeekPage(progress) - } - } - - /** - * 显示菜单 - */ - override fun showMenuBar() { - binding.readMenu.runMenuIn() - } - - override val oldBook: Book? - get() = ReadBook.book - - override fun changeTo(source: BookSource, book: Book, toc: List) { - if (!book.isAudio) { - viewModel.changeTo(book, toc) - } else { - ReadAloud.stop(this) - lifecycleScope.launch { - withContext(IO) { - ReadBook.book?.migrateTo(book, toc) - book.removeType(BookType.updateError) - ReadBook.book?.delete() - appDb.bookDao.insert(book) - } - startActivityForBook(book) - finish() - } - } - } - - override fun replaceContent(content: String) { - ReadBook.book?.let { - viewModel.saveContent(it, content) - } - } - - override fun showActionMenu() { - when { - BaseReadAloudService.isRun -> showReadAloudDialog() - isAutoPage -> showDialogFragment() - isShowingSearchResult -> binding.searchMenu.runMenuIn() - else -> binding.readMenu.runMenuIn() - } - } - - /** - * 显示朗读菜单 - */ - override fun showReadAloudDialog() { - showDialogFragment() - } - - /** - * 自动翻页 - */ - override fun autoPage() { - ReadAloud.stop(this) - if (isAutoPage) { - autoPageStop() - } else { - binding.readView.autoPager.start() - binding.readMenu.setAutoPage(true) - screenTimeOut = -1L - screenOffTimerStart() - } - } - - override fun autoPageStop() { - if (isAutoPage) { - binding.readView.autoPager.stop() - binding.readMenu.setAutoPage(false) - dismissDialogFragment() - upScreenTimeOut() - } - } - - override fun openSourceEditActivity() { - ReadBook.bookSource?.let { - sourceEditActivity.launch { - putExtra("sourceUrl", it.bookSourceUrl) - } - } - } - - override fun openBookInfoActivity() { - ReadBook.book?.let { - bookInfoActivity.launch { - putExtra("name", it.name) - putExtra("author", it.author) - putExtra("bookUrl", it.bookUrl) - } - } - } - - /** - * 替换 - */ - override fun openReplaceRule() { - replaceActivity.launch(Intent(this, ReplaceRuleActivity::class.java)) - } - - /** - * 打开目录 - */ - override fun openChapterList() { - ReadBook.book?.let { - tocActivity.launch(it.bookUrl) - } - } - - /** - * 打开搜索界面 - */ - override fun openSearchActivity(searchWord: String?) { - val book = ReadBook.book ?: return - searchContentActivity.launch { - putExtra("bookUrl", book.bookUrl) - putExtra("searchWord", searchWord) - putExtra("searchResultIndex", viewModel.searchResultIndex) - viewModel.searchResultList?.first()?.let { - if (it.query == viewModel.searchContentQuery) { - IntentData.put("searchResultList", viewModel.searchResultList) - } - } - } - } - - /** - * 禁用书源 - */ - override fun disableSource() { - viewModel.disableSource() - } - - /** - * 显示阅读样式配置 - */ -// override fun showReadStyle() { -// showDialogFragment() -// } -// -// /** -// * 显示更多设置 -// */ -// override fun showMoreSetting() { -// showDialogFragment() -// } -// - override fun showSearchSetting() { - showDialogFragment() - } - - override fun showReadStyle() { - showDialogFragment() - } - - /** - * 更新状态栏,导航栏 - */ - override fun upSystemUiVisibility() { - upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible) - upNavigationBarColor() - } - - // 退出全文搜索 - override fun exitSearchMenu() { - if (isShowingSearchResult) { - isShowingSearchResult = false - binding.searchMenu.invalidate() - binding.searchMenu.invisible() - ReadBook.clearSearchResult() - binding.readView.cancelSelect(true) - } - } - - /* 恢复到 全文搜索/进度条跳转前的位置 */ - private fun restoreLastBookProcess() { - if (confirmRestoreProcess == true) { - ReadBook.restoreLastBookProgress() - } else if (confirmRestoreProcess == null) { - alert(R.string.draw) { - setMessage(R.string.restore_last_book_process) - yesButton { - confirmRestoreProcess = true - ReadBook.restoreLastBookProgress() //恢复启动全文搜索前的进度 - } - noButton { - ReadBook.lastBookProgress = null - confirmRestoreProcess = false - } - onCancelled { - ReadBook.lastBookProgress = null - confirmRestoreProcess = false - } - } - } - } - - override fun showLogin() { - ReadBook.bookSource?.let { - startActivity { - putExtra("bookType", BookType.text) - } - } - } - - override fun payAction() { - val book = ReadBook.book ?: return - if (book.isLocal) return - val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) - if (chapter == null) { - toastOnUi("no chapter") - return - } - alert(R.string.chapter_pay) { - setMessage(chapter.title) - yesButton { - Coroutine.async(lifecycleScope) { - val source = - ReadBook.bookSource ?: throw NoStackTraceException("no book source") - val payAction = source.getContentRule().payAction - if (payAction.isNullOrBlank()) { - throw NoStackTraceException("no pay action") - } - val analyzeRule = AnalyzeRule(book, source) - analyzeRule.setCoroutineContext(coroutineContext) - analyzeRule.setBaseUrl(chapter.url) - analyzeRule.setChapter(chapter) - analyzeRule.evalJS(payAction).toString() - }.onSuccess(IO) { - if (it.isAbsUrl()) { - startActivity { - val bookSource = ReadBook.bookSource - putExtra("title", getString(R.string.chapter_pay)) - putExtra("url", it) - putExtra("sourceOrigin", bookSource?.bookSourceUrl) - putExtra("sourceName", bookSource?.bookSourceName) - putExtra("sourceType", bookSource?.getSourceType()) - } - } else if (it.isTrue()) { - //购买成功后刷新目录 - ReadBook.book?.let { - ReadBook.curTextChapter = null - BookHelp.delContent(book, chapter) - loadChapterList(book) - } - } - }.onError { - AppLog.put("执行购买操作出错\n${it.localizedMessage}", it, true) - } - } - noButton() - } - } - - /** - * 朗读按钮 - */ - override fun onClickReadAloud() { - autoPageStop() - when { - !BaseReadAloudService.isRun -> { - ReadAloud.upReadAloudClass() - val scrollPageAnim = ReadBook.pageAnim() == 3 - if (scrollPageAnim) { - val pos = binding.readView.getReadAloudPos() - if (pos != null) { - val (index, line) = pos - if (ReadBook.durChapterIndex != index) { - ReadBook.openChapter(index, line.chapterPosition, false) { - ReadBook.readAloud(startPos = line.pagePosition) - } - } else { - ReadBook.durChapterPos = line.chapterPosition - ReadBook.readAloud(startPos = line.pagePosition) - } - } else { - ReadBook.readAloud() - } - } else { - ReadBook.readAloud() - } - } - - BaseReadAloudService.pause -> { - val scrollPageAnim = ReadBook.pageAnim() == 3 - if (scrollPageAnim && pageChanged) { - pageChanged = false - val pos = binding.readView.getReadAloudPos() - if (pos != null) { - val (index, line) = pos - if (ReadBook.durChapterIndex != index) { - ReadBook.openChapter(index, line.chapterPosition, false) { - ReadBook.readAloud(startPos = line.pagePosition) - } - } else { - ReadBook.durChapterPos = line.chapterPosition - ReadBook.readAloud(startPos = line.pagePosition) - } - } else { - ReadBook.readAloud() - } - } else { - ReadAloud.resume(this) - } - } - - else -> ReadAloud.pause(this) - } - } - - override fun showHelp() { - showHelp("readMenuHelp") - } - - /** - * 点击图片 - */ - override fun oldClickImg(src: String): Boolean { - val urlMatcher = paramPattern.matcher(src) - if (urlMatcher.find()) { - val urlOptionStr = src.substring(urlMatcher.end()) - val urlOptionMap = GSON.fromJsonObject>(urlOptionStr).getOrNull() - val click = urlOptionMap?.get("click") - if (click != null) { - Coroutine.async(lifecycleScope, IO) { - val source = ReadBook.bookSource ?: return@async - val java = SourceLoginJsExtensions(this@ReadBookActivity, source, BookType.text) - val book = ReadBook.book ?: return@async - val chapter = - appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) - ?: throw Exception("no find chapter") - runScriptWithContext { - source.evalJS(click) { - put("java", java) - put("book", book) - put("chapter", chapter) - put("result", src) - } - } - }.onError { - AppLog.put("执行图片链接click键值出错\n${it.localizedMessage}", it, true) - } - return true - } - val jsStr = urlOptionMap?.get("js") ?: return false - Coroutine.async(lifecycleScope, IO) { - val source = ReadBook.bookSource ?: return@async - val book = ReadBook.book ?: return@async - val chapter = - appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) - ?: throw Exception("no find chapter") - val urlNoOption = src.take(urlMatcher.start()) - AnalyzeRule(book, source).apply { - setCoroutineContext(coroutineContext) - setBaseUrl(chapter.url) - setChapter(chapter) - evalJS(jsStr, urlNoOption) - } - }.onError { - AppLog.put("执行图片链接js键值出错\n${it.localizedMessage}", it, true) - } - return true - } - return false - } - - override fun clickImg(click: String, src: String) { - Coroutine.async(lifecycleScope, IO) { - val source = ReadBook.bookSource ?: return@async - val java = SourceLoginJsExtensions(this@ReadBookActivity, source, BookType.text) - val book = ReadBook.book ?: return@async - val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) - ?: throw Exception("no find chapter") - runScriptWithContext { - source.evalJS(click) { - put("java", java) - put("book", book) - put("chapter", chapter) - put("result", src) - } - } - }.onError { - AppLog.put("执行图片链接click键值出错\n${it.localizedMessage}", it, true) - } - } - - /** - * 长按图片 - */ - @SuppressLint("RtlHardcoded") - override fun onImageLongPress(x: Float, y: Float, src: String) { - binding.root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - popupAction.setItems( - listOf( - SelectItem(getString(R.string.show), "show"), - SelectItem(getString(R.string.refresh), "refresh"), - SelectItem("保存到相册", "save"), - SelectItem(getString(R.string.menu), "menu"), - ) - ) - popupAction.onActionClick = { - when (it) { - "show" -> showDialogFragment(PhotoDialog(src)) - "refresh" -> viewModel.refreshImage(src) - "save" -> { - viewModel.saveImage(src) - } - "menu" -> showActionMenu() - } - popupAction.dismiss() - } - val navigationBarHeight = - if (!ReadBookConfig.hideNavigationBar && navigationBarGravity == Gravity.BOTTOM) - binding.navigationBar.height else 0 - popupAction.showAtLocation( - binding.readView, Gravity.BOTTOM or Gravity.LEFT, x.toInt(), - binding.root.height + navigationBarHeight - y.toInt() - ) - } - - /** - * colorSelectDialog - */ - override fun onColorSelected(dialogId: Int, color: Int) = ReadBookConfig.durConfig.run { - when (dialogId) { - S_COLOR -> { - setCurShadColor(color) - postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) - } - - TEXT_COLOR -> { - setCurTextColor(color) - postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) - if (AppConfig.readBarStyleFollowPage) { - postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) - } - } - - TEXT_ACCENT_COLOR -> { - setCurTextAccentColor(color) - postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) - if (AppConfig.readBarStyleFollowPage) { - postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) - } - } - - BG_COLOR -> { - setCurBg(0, "#${color.hexString}") - postEvent(EventBus.UP_CONFIG, arrayListOf(1)) - if (AppConfig.readBarStyleFollowPage) { - postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) - } - } - - TIP_COLOR -> { - ReadTipConfig.tipColor = color - postEvent(EventBus.TIP_COLOR, "") - postEvent(EventBus.UP_CONFIG, arrayListOf(2)) - } - - TIP_DIVIDER_COLOR -> { - ReadTipConfig.tipDividerColor = color - postEvent(EventBus.TIP_COLOR, "") - postEvent(EventBus.UP_CONFIG, arrayListOf(2)) - } - - B_COLOR -> { - setMenuCurBg(color) - postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) - } - - A_COLOR -> { - setMenuCurAc(color) - postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) - } - - U_COLOR -> { - setUnderlineColor(color) - postEvent(EventBus.UP_CONFIG, arrayListOf(2)) - postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11)) - } - } - } - - /** - * colorSelectDialog - */ - override fun onDialogDismissed(dialogId: Int) = Unit - - override fun onTocRegexDialogResult(tocRegex: String) { - ReadBook.book?.let { - it.tocUrl = tocRegex - loadChapterList(it) - } - } - - private fun sureSyncProgress(progress: BookProgress) { - alert(R.string.get_book_progress) { - setMessage(R.string.current_progress_exceeds_cloud) - okButton { - ReadBook.setProgress(progress) - } - noButton() - } - } - - /* 进度条跳转到指定章节 */ - override fun skipToChapter(index: Int) { - ReadBook.saveCurrentBookProgress() //退出章节跳转恢复此时进度 - viewModel.openChapter(index) - } - - /* 全文搜索跳转 */ - override fun navigateToSearch(searchResult: SearchResult, index: Int) { - viewModel.searchResultIndex = index - skipToSearch(searchResult) - } - - override fun onMenuShow() { - binding.readView.autoPager.pause() - } - - override fun onMenuHide() { - binding.readView.autoPager.resume() - } - - override fun refresh() { - recreate() - } - - override fun onLayoutPageCompleted(index: Int, page: TextPage) { - upSeekBarThrottle.invoke() - binding.readView.onLayoutPageCompleted(index, page) - } - - /* 全文搜索跳转 */ - private fun skipToSearch(searchResult: SearchResult) { - if (searchResult.chapterIndex != ReadBook.durChapterIndex) { - viewModel.openChapter(searchResult.chapterIndex) { - jumpToPosition(searchResult) - } - } else { - jumpToPosition(searchResult) - } - } - - private fun jumpToPosition(searchResult: SearchResult) { - val curTextChapter = ReadBook.curTextChapter ?: return - binding.searchMenu.updateSearchInfo() - val (pageIndex, lineIndex, charIndex, addLine, charIndex2) = - viewModel.searchResultPositions(curTextChapter, searchResult) - ReadBook.skipToPage(pageIndex) { - isSelectingSearchResult = true - binding.readView.curPage.selectStartMoveIndex(0, lineIndex, charIndex) - when (addLine) { - 0 -> binding.readView.curPage.selectEndMoveIndex( - 0, - lineIndex, - charIndex + viewModel.searchContentQuery.length - 1 - ) - - 1 -> binding.readView.curPage.selectEndMoveIndex( - 0, lineIndex + 1, charIndex2 - ) - //consider change page, jump to scroll position - -1 -> binding.readView.curPage.selectEndMoveIndex(1, 0, charIndex2) - } - binding.readView.isTextSelected = true - isSelectingSearchResult = false - } - } - - override fun addBookmark() { - val book = ReadBook.book - val page = ReadBook.curTextChapter?.getPage(ReadBook.durPageIndex) - if (book != null && page != null) { - val bookmark = book.createBookMark().apply { - chapterIndex = ReadBook.durChapterIndex - chapterPos = ReadBook.durChapterPos - chapterName = page.title - bookText = page.text.replace(Regex("[袮꧁]"), "").trim() - } - showDialogFragment(BookmarkDialog(bookmark)) - } - } - - override fun changeReplaceRuleState() { - binding.readMenu.runMenuOut() - ReadBook.book?.let { - it.setUseReplaceRule(!it.getUseReplaceRule()) - ReadBook.saveRead() - menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.getUseReplaceRule() - binding.readMenu.changeReplace(it.getUseReplaceRule()) - viewModel.replaceRuleChanged() - } - } - - private fun startBackupJob() { - backupJob?.cancel() - backupJob = lifecycleScope.launch(IO) { - delay(300000) - ReadBook.book?.let { - AppWebDav.uploadBookProgress(it) - ensureActive() - it.update() - Backup.autoBack(this@ReadBookActivity) - } - } - } - - override fun sureNewProgress(progress: BookProgress) { - syncDialog?.dismiss() - syncDialog = alert(R.string.get_book_progress) { - setMessage(R.string.cloud_progress_exceeds_current) - okButton { - ReadBook.setProgress(progress) - } - noButton() - } - } - - override fun onDestroy() { - super.onDestroy() - tts?.clearTts() - textActionMenu.dismiss() - popupAction.dismiss() - binding.readView.onDestroy() - ReadBook.unregister(this) - if (!ReadBook.inBookshelf && !isChangingConfigurations) { - viewModel.removeFromBookshelf(null) - } - if (!BuildConfig.DEBUG) { - Backup.autoBack(this) - } - } - - override fun observeLiveBus() = binding.run { - observeEvent(EventBus.TIME_CHANGED) { readView.upTime() } - observeEvent(EventBus.BATTERY_CHANGED) { readView.upBattery(it) } - observeEvent(EventBus.MEDIA_BUTTON) { - if (it) { - onClickReadAloud() - } else { - ReadBook.readAloud(!BaseReadAloudService.pause) - } - } - observeEvent>(EventBus.UP_CONFIG) { - it.forEach { value -> - when (value) { - 0 -> upSystemUiVisibility() - 1 -> readView.upBg() - 2 -> { - readView.upStyle() - binding.readMenu.reset() - } - 3 -> readView.upBgAlpha() - 4 -> readView.upPageSlopSquare() - 5 -> if (isInitFinish) ReadBook.loadContent(resetPageOffset = false) - 6 -> readView.upContent(resetPageOffset = false) - 8 -> ChapterProvider.upStyle() - 9 -> readView.invalidateTextPage() - 10 -> ChapterProvider.upLayout() - 11 -> readView.submitRenderTask() - } - } - } - observeEvent(EventBus.ALOUD_STATE) { - if (it == Status.STOP || it == Status.PAUSE) { - ReadBook.curTextChapter?.let { textChapter -> - val page = textChapter.getPageByReadPos(ReadBook.durChapterPos) - if (page != null) { - page.removePageAloudSpan() - readView.upContent(resetPageOffset = false) - } - } - } - } - observeEventSticky(EventBus.TTS_PROGRESS) { chapterStart -> - lifecycleScope.launch(IO) { - if (BaseReadAloudService.isPlay()) { - ReadBook.curTextChapter?.let { textChapter -> - val pageIndex = ReadBook.durPageIndex - val aloudSpanStart = chapterStart - textChapter.getReadLength(pageIndex) - textChapter.getPage(pageIndex) - ?.upPageAloudSpan(aloudSpanStart) - upContent() - } - } - } - } - observeEvent(PreferKey.keepLight) { - upScreenTimeOut() - } - observeEvent(PreferKey.textSelectAble) { - readView.curPage.upSelectAble(it) - } - observeEvent(PreferKey.showBrightnessView) { - readMenu.upBrightnessState() - } - observeEvent>(EventBus.SEARCH_RESULT) { - viewModel.searchResultList = it - } - observeEvent(EventBus.UPDATE_READ_ACTION_BAR) { - readMenu.reset() - } - observeEvent(EventBus.UP_SEEK_BAR) { - readMenu.upSeekBar() - } - observeEvent(EventBus.REFRESH_BOOK_CONTENT) { - ReadBook.book?.let { - ReadBook.curTextChapter = null - binding.readView.upContent() - viewModel.refreshContentDur(it) - } - } - } - - private fun upScreenTimeOut() { - val keepLightPrefer = getPrefString(PreferKey.keepLight)?.toInt() ?: 0 - screenTimeOut = keepLightPrefer * 1000L - screenOffTimerStart() - } - - /** - * 重置黑屏时间 - */ - override fun screenOffTimerStart() { - handler.post { - if (screenTimeOut < 0) { - keepScreenOn(true) - return@post - } - val t = screenTimeOut - sysScreenOffTime - if (t > 0) { - keepScreenOn(true) - handler.removeCallbacks(screenOffRunnable) - handler.postDelayed(screenOffRunnable, screenTimeOut) - } else { - keepScreenOn(false) - } - } - } - - override fun addToBookshelf(book: Book, toc: List) { - viewModel.addToBookshelf(book, toc) { - toastOnUi("已添加到书架") - } - } - - companion object { - const val RESULT_DELETED = 100 - } - -} +package io.legado.app.ui.book.read + +//import io.legado.app.lib.theme.accentColor +import android.annotation.SuppressLint +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.os.Bundle +import android.view.Gravity +import android.view.HapticFeedbackConstants +import android.view.InputDevice +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import androidx.activity.addCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.get +import androidx.core.view.size +import androidx.lifecycle.lifecycleScope +import com.google.android.material.transition.platform.MaterialContainerTransform +import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback +import com.jaredrummler.android.colorpicker.ColorPickerDialogListener +import com.script.rhino.runScriptWithContext +import io.legado.app.BuildConfig +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.constant.Status +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.IntentData +import io.legado.app.help.TTS +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isEpub +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isLocalTxt +import io.legado.app.help.book.isMobi +import io.legado.app.help.book.removeType +import io.legado.app.help.book.update +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ReadTipConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.source.getSourceType +import io.legado.app.help.storage.Backup +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.paramPattern +import io.legado.app.model.localBook.EpubFile +import io.legado.app.model.localBook.MobiFile +import io.legado.app.receiver.NetworkChangedListener +import io.legado.app.receiver.TimeBatteryReceiver +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.bookmark.BookmarkDialog +import io.legado.app.ui.book.changesource.ChangeBookSourceDialog +import io.legado.app.ui.book.changesource.ChangeChapterSourceDialog +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.read.config.AutoReadDialog +import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR +import io.legado.app.ui.book.read.config.FontConfigDialog +import io.legado.app.ui.book.read.config.FontConfigDialog.Companion.S_COLOR +import io.legado.app.ui.book.read.config.FontConfigDialog.Companion.TEXT_ACCENT_COLOR +import io.legado.app.ui.book.read.config.FontConfigDialog.Companion.TEXT_COLOR +import io.legado.app.ui.book.read.config.FontSelectDialog +import io.legado.app.ui.book.read.config.ReadAloudDialog +import io.legado.app.ui.book.read.config.ReadStyleDialog +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.A_COLOR +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.B_COLOR +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR +import io.legado.app.ui.book.read.config.ToolButtonConfigDialog +import io.legado.app.ui.book.read.config.UnderlineConfigDialog.Companion.U_COLOR +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.LayoutProgressListener +import io.legado.app.ui.book.searchContent.SearchContentActivity +import io.legado.app.ui.book.searchContent.SearchResult +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog +import io.legado.app.ui.browser.WebViewActivity +import io.legado.app.ui.dict.DictDialog +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.login.SourceLoginJsExtensions +import io.legado.app.ui.replace.ReplaceEditRoute +import io.legado.app.ui.replace.ReplaceRuleActivity +import io.legado.app.ui.widget.PopupAction +import io.legado.app.ui.widget.dialog.PhotoDialog +import io.legado.app.utils.Debounce +import io.legado.app.utils.GSON +import io.legado.app.utils.LogUtils +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.applyOpenTint +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.dismissDialogFragment +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefString +import io.legado.app.utils.hexString +import io.legado.app.utils.iconItemOnLongClick +import io.legado.app.utils.invisible +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isTrue +import io.legado.app.utils.navigationBarGravity +import io.legado.app.utils.observeEvent +import io.legado.app.utils.observeEventSticky +import io.legado.app.utils.postEvent +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.startActivity +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.sysScreenOffTime +import io.legado.app.utils.themeColor +import io.legado.app.utils.throttle +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 阅读界面 + */ +class ReadBookActivity : BaseReadBookActivity(), + View.OnTouchListener, + ReadView.CallBack, + TextActionMenu.CallBack, + ContentTextView.CallBack, + PopupMenu.OnMenuItemClickListener, + ReadMenu.CallBack, + SearchMenu.CallBack, + ReadAloudDialog.CallBack, + ChangeBookSourceDialog.CallBack, + ChangeChapterSourceDialog.CallBack, + ReadBook.CallBack, + AutoReadDialog.CallBack, + ToolButtonConfigDialog.CallBack, + TxtTocRuleDialog.CallBack, + ColorPickerDialogListener, + FontConfigDialog.CallBack, + FontSelectDialog.CallBack, + LayoutProgressListener { + + private val tocActivity = + registerForActivityResult(TocActivityResult()) { + it?.let { + viewModel.openChapter(it.first, it.second) + } + } + private val sourceEditActivity = + registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) { + if (it.resultCode == RESULT_OK) { + viewModel.upBookSource { + upMenuView() + } + } + } + private val replaceActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + viewModel.replaceRuleChanged() + } + } + + private val searchContentActivity = + registerForActivityResult(StartActivityContract(SearchContentActivity::class.java)) { + val data = it.data ?: return@registerForActivityResult + val key = data.getLongExtra("key", System.currentTimeMillis()) + val index = data.getIntExtra("index", 0) + val searchResult = IntentData.get("searchResult$key") + val searchResultList = IntentData.get>("searchResultList$key") + if (searchResult != null && searchResultList != null) { + viewModel.searchContentQuery = searchResult.query + binding.searchMenu.upSearchResultList(searchResultList) + isShowingSearchResult = true + viewModel.searchResultIndex = index + binding.searchMenu.updateSearchResultIndex(index) + binding.searchMenu.selectedSearchResult?.let { currentResult -> + ReadBook.saveCurrentBookProgress() //退出全文搜索恢复此时进度 + skipToSearch(currentResult) + showActionMenu() + } + } + } + private val bookInfoActivity = + registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) { + if (it.resultCode == RESULT_OK) { + setResult(RESULT_DELETED) + finish() + } else { + ReadBook.loadOrUpContent() + } + } + private var menu: Menu? = null + private var backupJob: Job? = null + private var tts: TTS? = null + val textActionMenu: TextActionMenu by lazy { + TextActionMenu(this, this) + } + private val popupAction: PopupAction by lazy { + PopupAction(this) + } + + // 当前使用的字体路径(给 FontDialog 用) + override val curFontPath: String + get() = ReadBookConfig.textFont + + // 当选择了字体时被调用 + override fun selectFont(path: String) { + // path 为空表示恢复系统默认字体 + ReadBookConfig.textFont = path + // 通知阅读界面刷新字体 + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5, 2)) + } + + override val isInitFinish: Boolean get() = viewModel.isInitFinish + override val isScroll: Boolean get() = binding.readView.isScroll + private val isAutoPage get() = binding.readView.isAutoPage + override var isShowingSearchResult = false + override var isSelectingSearchResult = false + set(value) { + field = value && isShowingSearchResult + } + private val timeBatteryReceiver = TimeBatteryReceiver() + private var screenTimeOut: Long = 0 + private var loadStates: Boolean = false + override val pageFactory get() = binding.readView.pageFactory + override val pageDelegate get() = binding.readView.pageDelegate + override val headerHeight: Int get() = binding.readView.curPage.headerHeight + override val imgBgPaddingStart: Int get() = binding.readView.curPage.imgBgPaddingStart + private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } } + private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } } + private var bookChanged = false + private var pageChanged = false + private val handler by lazy { buildMainHandler() } + private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } } + private val executor = ReadBook.executor + private val upSeekBarThrottle = throttle(200) { + runOnUiThread { + upSeekBarProgress() + binding.readMenu.upSeekBar() + } + } + + //恢复跳转前进度对话框的交互结果 + private var confirmRestoreProcess: Boolean? = null + private val networkChangedListener by lazy { + NetworkChangedListener(this) + } + private var justInitData: Boolean = false + private var syncDialog: AlertDialog? = null + + private val delayMillis: Long = 500 + private var hasLoadedBook = false + + private fun ensureLoadBook() { + if (!hasLoadedBook) { + hasLoadedBook = true + loadBook() + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) + setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) + val transform = MaterialContainerTransform().apply { + addTarget(binding.root) + scrimColor = Color.TRANSPARENT + duration = delayMillis + } + window.sharedElementEnterTransition = transform + window.sharedElementReturnTransition = transform + + super.onCreate(savedInstanceState) + + binding.rootView.transitionName = intent.getStringExtra("transitionName") + + upScreenTimeOut() + ReadBook.register(this) + binding.readMenu.colorSurfaceContainer = + themeColor(com.google.android.material.R.attr.colorSurfaceContainer) + binding.readMenu.colorSecondary = + themeColor(androidx.appcompat.R.attr.colorPrimary) + binding.readMenu.colorSecondaryContainer = + themeColor(com.google.android.material.R.attr.colorSecondaryContainer) + binding.cursorLeft.setOnTouchListener(this) + binding.cursorRight.setOnTouchListener(this) + + onBackPressedDispatcher.addCallback(this) { + if (isShowingSearchResult) { + exitSearchMenu() + restoreLastBookProcess() + return@addCallback + } + //拦截返回供恢复阅读进度 + if (ReadBook.lastBookProgress != null && confirmRestoreProcess != false) { + restoreLastBookProcess() + return@addCallback + } + if (BaseReadAloudService.isPlay()) { + ReadAloud.pause(this@ReadBookActivity) + toastOnUi(R.string.read_aloud_pause) + return@addCallback + } + if (isAutoPage) { + autoPageStop() + return@addCallback + } + if (getPrefBoolean("disableReturnKey") && !menuLayoutIsVisible) { + return@addCallback + } + if (savedInstanceState != null || !ReadBook.inBookshelf) { + ReadBook.commitReadSession() + finish() + } else { + ReadBook.commitReadSession() + supportFinishAfterTransition() + } + //TODO: 有关测量相关问题 + //孩子们,我的水平不够,只能这样修复测量问题了 + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + if (!AppConfig.sharedElementEnterTransitionEnable || !AppConfig.delayBookLoadEnable) { + ensureLoadBook() + } + //虽然很扯但有用 + binding.rootView.postDelayed({ + ensureLoadBook() + }, delayMillis) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + viewModel.initData(intent) + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + upSystemUiVisibility() + if (hasFocus) { + binding.readMenu.upBrightnessState() + } else if (!menuLayoutIsVisible) { + ReadBook.cancelPreDownloadTask() + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + //upSystemUiVisibility() + binding.readView.upStyle() + recreate() + } + + + override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) { + if (!isTopResumedActivity) { + ReadBook.cancelPreDownloadTask() + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onResume() { + super.onResume() + ReadBook.readStartTime = System.currentTimeMillis() + ReadBook.initReadTime() + ReadBook.startAutoSaveSession() + if (bookChanged) { + bookChanged = false + ReadBook.callBack = this + viewModel.initData(intent) + justInitData = true + } else { + //web端阅读时,app处于阅读界面,本地记录会覆盖web保存的进度,在此处恢复 + ReadBook.webBookProgress?.let { + ReadBook.setProgress(it) + ReadBook.webBookProgress = null + } + } + upSystemUiVisibility() + registerReceiver(timeBatteryReceiver, timeBatteryReceiver.filter) + binding.readView.upTime() + screenOffTimerStart() + // 网络监听,当从无网切换到网络环境时同步进度(注意注册的同时就会收到监听,因此界面激活时无需重复执行同步操作) + networkChangedListener.register() + networkChangedListener.onNetworkChanged = { + // 当网络是可用状态且无需初始化时同步进度(初始化中已有同步进度逻辑) + if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData) { + ReadBook.syncProgress({ progress -> sureNewProgress(progress) }) + } + } + } + + override fun onPause() { + super.onPause() + autoPageStop() + backupJob?.cancel() + ReadBook.saveRead() + ReadBook.stopAutoSaveSession() + ReadBook.commitReadSession() + ReadBook.cancelPreDownloadTask() + unregisterReceiver(timeBatteryReceiver) + upSystemUiVisibility() + if (!BuildConfig.DEBUG) { + if (AppConfig.syncBookProgressPlus) { + ReadBook.syncProgress() + } else { + ReadBook.uploadProgress() + } + Backup.autoBack(this) + } + justInitData = false + networkChangedListener.unRegister() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_read, menu) + menu.iconItemOnLongClick(R.id.menu_change_source) { + PopupMenu(this, it).apply { + inflate(R.menu.book_read_change_source) + this.menu.applyOpenTint(this@ReadBookActivity) + setOnMenuItemClickListener(this@ReadBookActivity) + }.show() + } + menu.iconItemOnLongClick(R.id.menu_refresh) { + PopupMenu(this, it).apply { + inflate(R.menu.book_read_refresh) + this.menu.applyOpenTint(this@ReadBookActivity) + setOnMenuItemClickListener(this@ReadBookActivity) + }.show() + } + binding.readMenu.refreshMenuColorFilter() + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_same_title_removed)?.isChecked = + ReadBook.curTextChapter?.sameTitleRemoved == true + return super.onMenuOpened(featureId, menu) + } + + private fun loadBook() { + lifecycleScope.launch(Dispatchers.Default) { + viewModel.initReadBookConfig(intent) + viewModel.initData(intent) + withContext(Main) { + justInitData = true + } + } + } + + /** + * 更新菜单 + */ + private fun upMenu() { + val menu = menu ?: return + val book = ReadBook.book ?: return + val onLine = !book.isLocal + for (i in 0 until menu.size) { + val item = menu[i] + when (item.groupId) { + R.id.menu_group_on_line -> item.isVisible = onLine + R.id.menu_group_local -> item.isVisible = !onLine + R.id.menu_group_text -> item.isVisible = book.isLocalTxt + R.id.menu_group_epub -> item.isVisible = book.isEpub + else -> when (item.itemId) { + R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() + R.id.menu_re_segment -> item.isChecked = book.getReSegment() +// R.id.menu_enable_review -> { +// item.isVisible = BuildConfig.DEBUG +// item.isChecked = AppConfig.enableReview +// } + + R.id.menu_reverse_content -> item.isVisible = onLine + R.id.menu_del_ruby_tag -> item.isChecked = book.getDelTag(Book.rubyTag) + R.id.menu_del_h_tag -> item.isChecked = book.getDelTag(Book.hTag) + } + } + } + lifecycleScope.launch { + menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) { + AppWebDav.isOk + } + menu.findItem(R.id.menu_cover_progress)?.isVisible = withContext(IO) { + AppWebDav.isOk + } + } + } + + private fun defaultChangeSource() { + if (AppConfig.defaultSourceChangeAll) + { + binding.readMenu.runMenuOut() + ReadBook.book?.let { + showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) + } + } else { + lifecycleScope.launch { + val book = ReadBook.book ?: return@launch + val chapter = + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: return@launch + binding.readMenu.runMenuOut() + showDialogFragment( + ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title) + ) + } + } + } + + /** + * 菜单 + */ + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_change_source -> { defaultChangeSource() } + + R.id.menu_book_change_source -> { + binding.readMenu.runMenuOut() + ReadBook.book?.let { + showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) + } + } + + R.id.menu_chapter_change_source -> lifecycleScope.launch { + val book = ReadBook.book ?: return@launch + val chapter = + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: return@launch + binding.readMenu.runMenuOut() + showDialogFragment( + ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title) + ) + } + + R.id.menu_refresh, + R.id.menu_refresh_dur -> { + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + ReadBook.curTextChapter = null + binding.readView.upContent() + viewModel.refreshContentDur(it) + } + } + } + + R.id.menu_refresh_after -> { + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + ReadBook.clearTextChapter() + binding.readView.upContent() + viewModel.refreshContentAfter(it) + } + } + } + + R.id.menu_refresh_all -> { + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + refreshContentAll(it) + } + } + } + + R.id.menu_setting_replace -> openReplaceRule() + R.id.menu_download -> showDownloadDialog() + R.id.menu_add_bookmark -> addBookmark() + R.id.menu_simulated_reading -> showSimulatedReading() + R.id.menu_edit_content -> { + binding.readMenu.runMenuOut() + showDialogFragment(ContentEditDialog()) + } + R.id.menu_update_toc -> ReadBook.book?.let { + if (it.isEpub) { + BookHelp.clearCache(it) + EpubFile.clear() + } + if (it.isMobi) { + MobiFile.clear() + } + loadChapterList(it) + } + R.id.menu_enable_replace -> { + changeReplaceRuleState() + } + R.id.menu_re_segment -> ReadBook.book?.let { + it.setReSegment(!it.getReSegment()) + item.isChecked = it.getReSegment() + ReadBook.loadContent(false) + } + + R.id.menu_tool_button -> { + binding.readMenu.runMenuOut() + showDialogFragment(ToolButtonConfigDialog()) + } + +// R.id.menu_enable_review -> { +// AppConfig.enableReview = !AppConfig.enableReview +// item.isChecked = AppConfig.enableReview +// ReadBook.loadContent(false) +// } + + R.id.menu_del_ruby_tag -> ReadBook.book?.let { + item.isChecked = !item.isChecked + if (item.isChecked) { + it.addDelTag(Book.rubyTag) + } else { + it.removeDelTag(Book.rubyTag) + } + refreshContentAll(it) + } + + R.id.menu_del_h_tag -> ReadBook.book?.let { + item.isChecked = !item.isChecked + if (item.isChecked) { + it.addDelTag(Book.hTag) + } else { + it.removeDelTag(Book.hTag) + } + refreshContentAll(it) + } + + R.id.menu_page_anim -> showPageAnimConfig { + binding.readView.upPageAnim() + ReadBook.loadContent(false) + } + + R.id.menu_log -> showDialogFragment() + R.id.menu_toc_regex -> showDialogFragment( + TxtTocRuleDialog(ReadBook.book?.tocUrl) + ) + + R.id.menu_reverse_content -> ReadBook.book?.let { + viewModel.reverseContent(it) + } + + R.id.menu_set_charset -> showCharsetConfig() + R.id.menu_image_style -> { + val imgStyles = + arrayListOf( + Book.imgStyleDefault, Book.imgStyleFull, Book.imgStyleText, + Book.imgStyleSingle + ) + selector( + R.string.image_style, + imgStyles + ) { _, index -> + val imageStyle = imgStyles[index] + ReadBook.book?.setImageStyle(imageStyle) + if (imageStyle == Book.imgStyleSingle) { + ReadBook.book?.setPageAnim(0) // 切换图片样式single后,自动切换为覆盖 + binding.readView.upPageAnim() + } + ReadBook.loadContent(false) + } + } + + R.id.menu_get_progress -> ReadBook.book?.let { + viewModel.syncBookProgress(it) { progress -> + sureSyncProgress(progress) + } + } + + R.id.menu_cover_progress -> ReadBook.book?.let { + ReadBook.uploadProgress(true) { toastOnUi(R.string.upload_book_success) } + } + + R.id.menu_same_title_removed -> { + ReadBook.book?.let { + val contentProcessor = ContentProcessor.get(it) + val textChapter = ReadBook.curTextChapter + if (textChapter != null + && !textChapter.sameTitleRemoved + && !contentProcessor.removeSameTitleCache.contains( + textChapter.chapter.getFileName("nr") + ) + ) { + toastOnUi("未找到可移除的重复标题") + } + } + viewModel.reverseRemoveSameTitle() + } + + R.id.menu_effective_replaces -> showDialogFragment() + + R.id.menu_help -> showHelp() + } + return super.onCompatOptionsItemSelected(item) + } + + private fun refreshContentAll(book: Book) { + ReadBook.clearTextChapter() + binding.readView.upContent() + viewModel.refreshContentAll(book) + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + return onCompatOptionsItemSelected(item) + } + + /** + * 按键拦截,显示菜单 + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val keyCode = event.keyCode + val action = event.action + val isDown = action == 0 + + if (keyCode == KeyEvent.KEYCODE_MENU) { + if (isDown && !binding.readMenu.canShowMenu) { + binding.readMenu.runMenuIn() + return true + } + if (!isDown && !binding.readMenu.canShowMenu) { + binding.readMenu.canShowMenu = true + return true + } + } + return super.dispatchKeyEvent(event) + } + + /** + * 鼠标滚轮事件 + */ + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (0 != (event.source and InputDevice.SOURCE_CLASS_POINTER)) { + if (event.action == MotionEvent.ACTION_SCROLL) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + LogUtils.d("onGenericMotionEvent", "axisValue = $axisValue") + // 获得垂直坐标上的滚动方向 + if (axisValue < 0.0f) { // 滚轮向下滚 + mouseWheelPage(PageDirection.NEXT) + } else { // 滚轮向上滚 + mouseWheelPage(PageDirection.PREV) + } + return true + } + } + // 手柄摇杆控制翻页 + if (0 != (event.source and InputDevice.SOURCE_CLASS_JOYSTICK)) { + if (event.action == MotionEvent.ACTION_MOVE) { + // 左摇杆上下移动控制翻页 + val yAxis = event.getAxisValue(MotionEvent.AXIS_Y) + if (Math.abs(yAxis) > 0.5f) { // 死区设置 + if (yAxis > 0) { // 摇杆向下 + handleKeyPage(PageDirection.NEXT, false) + } else { // 摇杆向上 + handleKeyPage(PageDirection.PREV, false) + } + return true + } + } + } + return super.onGenericMotionEvent(event) + } + + /** + * 按键事件 + */ + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (menuLayoutIsVisible) { + return super.onKeyDown(keyCode, event) + } + val longPress = event.repeatCount > 0 + when { + isPrevKey(keyCode) -> { + handleKeyPage(PageDirection.PREV, longPress) + return true + } + + isNextKey(keyCode) -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + } + when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP -> if (volumeKeyPage(PageDirection.PREV, longPress)) { + return true + } + + KeyEvent.KEYCODE_VOLUME_DOWN -> if (volumeKeyPage(PageDirection.NEXT, longPress)) { + return true + } + + KeyEvent.KEYCODE_PAGE_UP -> { + handleKeyPage(PageDirection.PREV, longPress) + return true + } + + KeyEvent.KEYCODE_PAGE_DOWN -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + + KeyEvent.KEYCODE_SPACE -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + // 手柄方向键控制翻页 + KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_LEFT -> { + handleKeyPage(PageDirection.PREV, longPress) + return true + } + + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + } + + return super.onKeyDown(keyCode, event) + } + + /** + * 松开按键事件 + */ + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (volumeKeyPage(PageDirection.NONE, false)) { + return true + } + } + + } + return super.onKeyUp(keyCode, event) + } + + /** + * view触摸,文字选择 + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean = binding.run { + if (!binding.readView.isTextSelected) { + return false + } + when (event.action) { + MotionEvent.ACTION_DOWN -> textActionMenu.dismiss() + MotionEvent.ACTION_MOVE -> { + when (v.id) { + R.id.cursor_left -> if (!readView.curPage.getReverseStartCursor()) { + readView.curPage.selectStartMove( + event.rawX + cursorLeft.width, + event.rawY - cursorLeft.height + ) + } else { + readView.curPage.selectEndMove( + event.rawX - cursorRight.width, + event.rawY - cursorRight.height + ) + } + + R.id.cursor_right -> if (readView.curPage.getReverseEndCursor()) { + readView.curPage.selectStartMove( + event.rawX + cursorLeft.width, + event.rawY - cursorLeft.height + ) + } else { + readView.curPage.selectEndMove( + event.rawX - cursorRight.width, + event.rawY - cursorRight.height + ) + } + } + } + + MotionEvent.ACTION_UP -> { + readView.curPage.resetReverseCursor() + showTextActionMenu() + } + } + return true + } + + /** + * 更新文字选择开始位置 + */ + override fun upSelectedStart(x: Float, y: Float, top: Float): Unit = binding.run { + cursorLeft.x = x - cursorLeft.width + cursorLeft.y = y + cursorLeft.visible(true) + textMenuPosition.x = x + textMenuPosition.y = top + + root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + + /** + * 更新文字选择结束位置 + */ + override fun upSelectedEnd(x: Float, y: Float): Unit = binding.run { + cursorRight.x = x + cursorRight.y = y + cursorRight.visible(true) + + root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + + /** + * 取消文字选择 + */ + override fun onCancelSelect() = binding.run { + cursorLeft.invisible() + cursorRight.invisible() + textActionMenu.dismiss() + } + + override fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean { + return binding.readView.onTouchEvent(event) + } + + /** + * 显示文本操作菜单 + */ + override fun showTextActionMenu() { + val navigationBarHeight = + if (!ReadBookConfig.hideNavigationBar && navigationBarGravity == Gravity.BOTTOM) + binding.navigationBar.height else 0 + textActionMenu.show( + binding.textMenuPosition, + binding.root.height + navigationBarHeight, + binding.textMenuPosition.x.toInt(), + binding.textMenuPosition.y.toInt(), + binding.cursorLeft.y.toInt() + binding.cursorLeft.height, + binding.cursorRight.x.toInt(), + binding.cursorRight.y.toInt() + binding.cursorRight.height + ) + } + + /** + * 当前选择的文本 + */ + override val selectedText: String get() = binding.readView.getSelectText() + + /** + * 文本选择菜单操作 + */ + override fun onMenuItemSelected(itemId: Int): Boolean { + when (itemId) { + R.id.menu_aloud -> when (AppConfig.contentSelectSpeakMod) { + 1 -> lifecycleScope.launch { + binding.readView.aloudStartSelect() + } + + else -> speak(binding.readView.getSelectText()) + } + + R.id.menu_bookmark -> binding.readView.curPage.let { + val bookmark = it.createBookmark() + if (bookmark == null) { + toastOnUi(R.string.create_bookmark_error) + } else { + showDialogFragment(BookmarkDialog(bookmark)) + } + return true + } + + R.id.menu_edit -> { + val startPos = ReadBook.durChapterPos + showDialogFragment { + putInt("start_position", startPos) + putString("selected_text", binding.readView.getSelectText()) + } + return true + } + + R.id.menu_replace -> { + val scopes = arrayListOf() + ReadBook.book?.name?.let { scopes.add(it) } + ReadBook.bookSource?.bookSourceUrl?.let { scopes.add(it) } + + val text = selectedText.lineSequence().map { it.trim() }.joinToString("\n") + + val editRoute = ReplaceEditRoute( + id = -1, + pattern = text, + scope = scopes.joinToString(";"), + isScopeTitle = false, + isScopeContent = true + ) + + replaceActivity.launch( + ReplaceRuleActivity.startIntent( + context = this, + editRoute = editRoute + ) + ) + return true + } + + R.id.menu_search_content -> { + viewModel.searchContentQuery = selectedText + openSearchActivity(selectedText) + return true + } + + R.id.menu_dict -> { + showDialogFragment(DictDialog(selectedText)) + return true + } + } + return false + } + + private fun onEditTextAction(selectedText: String, startPos: Int) { + val bundle = Bundle().apply { + putString("selected_text", selectedText) + putInt("start_position", startPos) + } + + val dialog = ContentEditDialog().apply { + arguments = bundle + } + + dialog.show(supportFragmentManager, "ContentEditDialog") + } + + /** + * 文本选择菜单操作完成 + */ + override fun onMenuActionFinally() = binding.run { + textActionMenu.dismiss() + readView.cancelSelect() + } + + private fun speak(text: String) { + if (tts == null) { + tts = TTS() + } + tts?.speak(text) + } + + /** + * 鼠标滚轮翻页 + */ + private fun mouseWheelPage(direction: PageDirection) { + if (menuLayoutIsVisible || !AppConfig.mouseWheelPage) { + return + } + keyPageDebounce(direction, mouseWheel = true, longPress = false) + } + + /** + * 音量键翻页 + */ + private fun volumeKeyPage(direction: PageDirection, longPress: Boolean): Boolean { + if (!AppConfig.volumeKeyPage) { + return false + } + if (!AppConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) { + return false + } + handleKeyPage(direction, longPress) + return true + } + + private fun handleKeyPage(direction: PageDirection, longPress: Boolean) { + if (AppConfig.keyPageOnLongPress || direction == PageDirection.NONE) { + keyPage(direction) + } else { + keyPageDebounce(direction, longPress = longPress) + } + } + + private fun keyPageDebounce( + direction: PageDirection, + mouseWheel: Boolean = false, + longPress: Boolean + ) { + if (longPress) { + return + } + nextPageDebounce.apply { + wait = if (mouseWheel) 200L else 600L + leading = !mouseWheel + trailing = mouseWheel + } + prevPageDebounce.apply { + wait = if (mouseWheel) 200L else 600L + leading = !mouseWheel + trailing = mouseWheel + } + when (direction) { + PageDirection.NEXT -> nextPageDebounce.invoke() + PageDirection.PREV -> prevPageDebounce.invoke() + else -> {} + } + } + + private fun keyPage(direction: PageDirection) { + binding.readView.cancelSelect() + binding.readView.pageDelegate?.isCancel = false + binding.readView.pageDelegate?.keyTurnPage(direction) + } + + override fun upMenuView() { + handler.post { + upMenu() + binding.readMenu.upBookView() + } + } + + override fun loadChapterList(book: Book) { + ReadBook.upMsg(getString(R.string.toc_updateing)) + viewModel.loadChapterList(book) + } + + /** + * 内容加载完成 + */ + override fun contentLoadFinish() { + if (intent.getBooleanExtra("readAloud", false)) { + intent.removeExtra("readAloud") + ReadBook.readAloud() + } + loadStates = true + } + + /** + * 更新内容 + */ + override fun upContent( + relativePosition: Int, + resetPageOffset: Boolean, + success: (() -> Unit)? + ) { + lifecycleScope.launch { + binding.readView.upContent(relativePosition, resetPageOffset) + if (relativePosition == 0) { + upSeekBarProgress() + } + loadStates = false + success?.invoke() + } + } + + override suspend fun upContentAwait( + relativePosition: Int, + resetPageOffset: Boolean, + success: (() -> Unit)? + ) = withContext(Main.immediate) { + binding.readView.upContent(relativePosition, resetPageOffset) + if (relativePosition == 0) { + upSeekBarProgress() + } + loadStates = false + } + + override fun upPageAnim(upRecorder: Boolean) { + lifecycleScope.launch { + binding.readView.upPageAnim(upRecorder) + } + } + + override fun notifyBookChanged() { + bookChanged = true + if (!ReadBook.inBookshelf) { + viewModel.removeFromBookshelf { super.finish() } + } + } + + override fun cancelSelect() { + runOnUiThread { + binding.readView.cancelSelect() + } + } + + /** + * 页面改变 + */ + override fun pageChanged() { + pageChanged = true + binding.readView.onPageChange() + handler.post { + upSeekBarProgress() + } + executor.execute { + startBackupJob() + } + } + + /** + * 更新进度条位置 + */ + private fun upSeekBarProgress() { + val progress = when (AppConfig.progressBarBehavior) { + "page" -> ReadBook.durPageIndex + else /* chapter */ -> ReadBook.durChapterIndex + } + if (progress >= 0) { + binding.readMenu.setSeekPage(progress) + } + } + + /** + * 显示菜单 + */ + override fun showMenuBar() { + binding.readMenu.runMenuIn() + } + + override val oldBook: Book? + get() = ReadBook.book + + override fun changeTo(source: BookSource, book: Book, toc: List) { + if (!book.isAudio) { + viewModel.changeTo(book, toc) + } else { + ReadAloud.stop(this) + lifecycleScope.launch { + withContext(IO) { + ReadBook.book?.migrateTo(book, toc) + book.removeType(BookType.updateError) + ReadBook.book?.delete() + appDb.bookDao.insert(book) + } + startActivityForBook(book) + finish() + } + } + } + + override fun replaceContent(content: String) { + ReadBook.book?.let { + viewModel.saveContent(it, content) + } + } + + override fun showActionMenu() { + when { + BaseReadAloudService.isRun -> showReadAloudDialog() + isAutoPage -> showDialogFragment() + isShowingSearchResult -> binding.searchMenu.runMenuIn() + else -> binding.readMenu.runMenuIn() + } + } + + /** + * 显示朗读菜单 + */ + override fun showReadAloudDialog() { + showDialogFragment() + } + + /** + * 自动翻页 + */ + override fun autoPage() { + ReadAloud.stop(this) + if (isAutoPage) { + autoPageStop() + } else { + binding.readView.autoPager.start() + binding.readMenu.setAutoPage(true) + screenTimeOut = -1L + screenOffTimerStart() + } + } + + override fun autoPageStop() { + if (isAutoPage) { + binding.readView.autoPager.stop() + binding.readMenu.setAutoPage(false) + dismissDialogFragment() + upScreenTimeOut() + } + } + + override fun openSourceEditActivity() { + ReadBook.bookSource?.let { + sourceEditActivity.launch { + putExtra("sourceUrl", it.bookSourceUrl) + } + } + } + + override fun openBookInfoActivity() { + ReadBook.book?.let { + bookInfoActivity.launch { + putExtra("name", it.name) + putExtra("author", it.author) + putExtra("bookUrl", it.bookUrl) + } + } + } + + /** + * 替换 + */ + override fun openReplaceRule() { + replaceActivity.launch(Intent(this, ReplaceRuleActivity::class.java)) + } + + /** + * 打开目录 + */ + override fun openChapterList() { + ReadBook.book?.let { + tocActivity.launch(it.bookUrl) + } + } + + /** + * 打开搜索界面 + */ + override fun openSearchActivity(searchWord: String?) { + val book = ReadBook.book ?: return + searchContentActivity.launch { + putExtra("bookUrl", book.bookUrl) + putExtra("searchWord", searchWord) + putExtra("searchResultIndex", viewModel.searchResultIndex) + viewModel.searchResultList?.first()?.let { + if (it.query == viewModel.searchContentQuery) { + IntentData.put("searchResultList", viewModel.searchResultList) + } + } + } + } + + /** + * 禁用书源 + */ + override fun disableSource() { + viewModel.disableSource() + } + + /** + * 显示阅读样式配置 + */ +// override fun showReadStyle() { +// showDialogFragment() +// } +// +// /** +// * 显示更多设置 +// */ +// override fun showMoreSetting() { +// showDialogFragment() +// } +// + override fun showSearchSetting() { + showDialogFragment() + } + + override fun showReadStyle() { + showDialogFragment() + } + + /** + * 更新状态栏,导航栏 + */ + override fun upSystemUiVisibility() { + upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible) + upNavigationBarColor() + } + + // 退出全文搜索 + override fun exitSearchMenu() { + if (isShowingSearchResult) { + isShowingSearchResult = false + binding.searchMenu.invalidate() + binding.searchMenu.invisible() + ReadBook.clearSearchResult() + binding.readView.cancelSelect(true) + } + } + + /* 恢复到 全文搜索/进度条跳转前的位置 */ + private fun restoreLastBookProcess() { + if (confirmRestoreProcess == true) { + ReadBook.restoreLastBookProgress() + } else if (confirmRestoreProcess == null) { + alert(R.string.draw) { + setMessage(R.string.restore_last_book_process) + yesButton { + confirmRestoreProcess = true + ReadBook.restoreLastBookProgress() //恢复启动全文搜索前的进度 + } + noButton { + ReadBook.lastBookProgress = null + confirmRestoreProcess = false + } + onCancelled { + ReadBook.lastBookProgress = null + confirmRestoreProcess = false + } + } + } + } + + override fun showLogin() { + ReadBook.bookSource?.let { + startActivity { + putExtra("bookType", BookType.text) + } + } + } + + override fun payAction() { + val book = ReadBook.book ?: return + if (book.isLocal) return + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + if (chapter == null) { + toastOnUi("no chapter") + return + } + alert(R.string.chapter_pay) { + setMessage(chapter.title) + yesButton { + Coroutine.async(lifecycleScope) { + val source = + ReadBook.bookSource ?: throw NoStackTraceException("no book source") + val payAction = source.getContentRule().payAction + if (payAction.isNullOrBlank()) { + throw NoStackTraceException("no pay action") + } + val analyzeRule = AnalyzeRule(book, source) + analyzeRule.setCoroutineContext(coroutineContext) + analyzeRule.setBaseUrl(chapter.url) + analyzeRule.setChapter(chapter) + analyzeRule.evalJS(payAction).toString() + }.onSuccess(IO) { + if (it.isAbsUrl()) { + startActivity { + val bookSource = ReadBook.bookSource + putExtra("title", getString(R.string.chapter_pay)) + putExtra("url", it) + putExtra("sourceOrigin", bookSource?.bookSourceUrl) + putExtra("sourceName", bookSource?.bookSourceName) + putExtra("sourceType", bookSource?.getSourceType()) + } + } else if (it.isTrue()) { + //购买成功后刷新目录 + ReadBook.book?.let { + ReadBook.curTextChapter = null + BookHelp.delContent(book, chapter) + loadChapterList(book) + } + } + }.onError { + AppLog.put("执行购买操作出错\n${it.localizedMessage}", it, true) + } + } + noButton() + } + } + + /** + * 朗读按钮 + */ + override fun onClickReadAloud() { + autoPageStop() + when { + !BaseReadAloudService.isRun -> { + ReadAloud.upReadAloudClass() + val scrollPageAnim = ReadBook.pageAnim() == 3 + if (scrollPageAnim) { + val pos = binding.readView.getReadAloudPos() + if (pos != null) { + val (index, line) = pos + if (ReadBook.durChapterIndex != index) { + ReadBook.openChapter(index, line.chapterPosition, false) { + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.durChapterPos = line.chapterPosition + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.readAloud() + } + } else { + ReadBook.readAloud() + } + } + + BaseReadAloudService.pause -> { + val scrollPageAnim = ReadBook.pageAnim() == 3 + if (scrollPageAnim && pageChanged) { + pageChanged = false + val pos = binding.readView.getReadAloudPos() + if (pos != null) { + val (index, line) = pos + if (ReadBook.durChapterIndex != index) { + ReadBook.openChapter(index, line.chapterPosition, false) { + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.durChapterPos = line.chapterPosition + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.readAloud() + } + } else { + ReadAloud.resume(this) + } + } + + else -> ReadAloud.pause(this) + } + } + + override fun showHelp() { + showHelp("readMenuHelp") + } + + /** + * 点击图片 + */ + override fun oldClickImg(src: String): Boolean { + val urlMatcher = paramPattern.matcher(src) + if (urlMatcher.find()) { + val urlOptionStr = src.substring(urlMatcher.end()) + val urlOptionMap = GSON.fromJsonObject>(urlOptionStr).getOrNull() + val click = urlOptionMap?.get("click") + if (click != null) { + Coroutine.async(lifecycleScope, IO) { + val source = ReadBook.bookSource ?: return@async + val java = SourceLoginJsExtensions(this@ReadBookActivity, source, BookType.text) + val book = ReadBook.book ?: return@async + val chapter = + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: throw Exception("no find chapter") + runScriptWithContext { + source.evalJS(click) { + put("java", java) + put("book", book) + put("chapter", chapter) + put("result", src) + } + } + }.onError { + AppLog.put("执行图片链接click键值出错\n${it.localizedMessage}", it, true) + } + return true + } + val jsStr = urlOptionMap?.get("js") ?: return false + Coroutine.async(lifecycleScope, IO) { + val source = ReadBook.bookSource ?: return@async + val book = ReadBook.book ?: return@async + val chapter = + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: throw Exception("no find chapter") + val urlNoOption = src.take(urlMatcher.start()) + AnalyzeRule(book, source).apply { + setCoroutineContext(coroutineContext) + setBaseUrl(chapter.url) + setChapter(chapter) + evalJS(jsStr, urlNoOption) + } + }.onError { + AppLog.put("执行图片链接js键值出错\n${it.localizedMessage}", it, true) + } + return true + } + return false + } + + override fun clickImg(click: String, src: String) { + Coroutine.async(lifecycleScope, IO) { + val source = ReadBook.bookSource ?: return@async + val java = SourceLoginJsExtensions(this@ReadBookActivity, source, BookType.text) + val book = ReadBook.book ?: return@async + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: throw Exception("no find chapter") + runScriptWithContext { + source.evalJS(click) { + put("java", java) + put("book", book) + put("chapter", chapter) + put("result", src) + } + } + }.onError { + AppLog.put("执行图片链接click键值出错\n${it.localizedMessage}", it, true) + } + } + + /** + * 长按图片 + */ + @SuppressLint("RtlHardcoded") + override fun onImageLongPress(x: Float, y: Float, src: String) { + binding.root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + popupAction.setItems( + listOf( + SelectItem(getString(R.string.show), "show"), + SelectItem(getString(R.string.refresh), "refresh"), + SelectItem("保存到相册", "save"), + SelectItem(getString(R.string.menu), "menu"), + ) + ) + popupAction.onActionClick = { + when (it) { + "show" -> showDialogFragment(PhotoDialog(src)) + "refresh" -> viewModel.refreshImage(src) + "save" -> { + viewModel.saveImage(src) + } + "menu" -> showActionMenu() + } + popupAction.dismiss() + } + val navigationBarHeight = + if (!ReadBookConfig.hideNavigationBar && navigationBarGravity == Gravity.BOTTOM) + binding.navigationBar.height else 0 + popupAction.showAtLocation( + binding.readView, Gravity.BOTTOM or Gravity.LEFT, x.toInt(), + binding.root.height + navigationBarHeight - y.toInt() + ) + } + + /** + * colorSelectDialog + */ + override fun onColorSelected(dialogId: Int, color: Int) = ReadBookConfig.durConfig.run { + when (dialogId) { + S_COLOR -> { + setCurShadColor(color) + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) + } + + TEXT_COLOR -> { + setCurTextColor(color) + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + + TEXT_ACCENT_COLOR -> { + setCurTextAccentColor(color) + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + + BG_COLOR -> { + setCurBg(0, "#${color.hexString}") + postEvent(EventBus.UP_CONFIG, arrayListOf(1)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + + TIP_COLOR -> { + ReadTipConfig.tipColor = color + postEvent(EventBus.TIP_COLOR, "") + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + + TIP_DIVIDER_COLOR -> { + ReadTipConfig.tipDividerColor = color + postEvent(EventBus.TIP_COLOR, "") + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + + B_COLOR -> { + setMenuCurBg(color) + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + + A_COLOR -> { + setMenuCurAc(color) + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + + U_COLOR -> { + setUnderlineColor(color) + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11)) + } + } + } + + /** + * colorSelectDialog + */ + override fun onDialogDismissed(dialogId: Int) = Unit + + override fun onTocRegexDialogResult(tocRegex: String) { + ReadBook.book?.let { + it.tocUrl = tocRegex + loadChapterList(it) + } + } + + private fun sureSyncProgress(progress: BookProgress) { + alert(R.string.get_book_progress) { + setMessage(R.string.current_progress_exceeds_cloud) + okButton { + ReadBook.setProgress(progress) + } + noButton() + } + } + + /* 进度条跳转到指定章节 */ + override fun skipToChapter(index: Int) { + ReadBook.saveCurrentBookProgress() //退出章节跳转恢复此时进度 + viewModel.openChapter(index) + } + + /* 全文搜索跳转 */ + override fun navigateToSearch(searchResult: SearchResult, index: Int) { + viewModel.searchResultIndex = index + skipToSearch(searchResult) + } + + override fun onMenuShow() { + binding.readView.autoPager.pause() + } + + override fun onMenuHide() { + binding.readView.autoPager.resume() + } + + override fun refresh() { + recreate() + } + + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + upSeekBarThrottle.invoke() + binding.readView.onLayoutPageCompleted(index, page) + } + + /* 全文搜索跳转 */ + private fun skipToSearch(searchResult: SearchResult) { + if (searchResult.chapterIndex != ReadBook.durChapterIndex) { + viewModel.openChapter(searchResult.chapterIndex) { + jumpToPosition(searchResult) + } + } else { + jumpToPosition(searchResult) + } + } + + private fun jumpToPosition(searchResult: SearchResult) { + val curTextChapter = ReadBook.curTextChapter ?: return + binding.searchMenu.updateSearchInfo() + val (pageIndex, lineIndex, charIndex, addLine, charIndex2) = + viewModel.searchResultPositions(curTextChapter, searchResult) + ReadBook.skipToPage(pageIndex) { + isSelectingSearchResult = true + binding.readView.curPage.selectStartMoveIndex(0, lineIndex, charIndex) + when (addLine) { + 0 -> binding.readView.curPage.selectEndMoveIndex( + 0, + lineIndex, + charIndex + viewModel.searchContentQuery.length - 1 + ) + + 1 -> binding.readView.curPage.selectEndMoveIndex( + 0, lineIndex + 1, charIndex2 + ) + //consider change page, jump to scroll position + -1 -> binding.readView.curPage.selectEndMoveIndex(1, 0, charIndex2) + } + binding.readView.isTextSelected = true + isSelectingSearchResult = false + } + } + + override fun addBookmark() { + val book = ReadBook.book + val page = ReadBook.curTextChapter?.getPage(ReadBook.durPageIndex) + if (book != null && page != null) { + val bookmark = book.createBookMark().apply { + chapterIndex = ReadBook.durChapterIndex + chapterPos = ReadBook.durChapterPos + chapterName = page.title + bookText = page.text.replace(Regex("[袮꧁]"), "").trim() + } + showDialogFragment(BookmarkDialog(bookmark)) + } + } + + override fun changeReplaceRuleState() { + binding.readMenu.runMenuOut() + ReadBook.book?.let { + it.setUseReplaceRule(!it.getUseReplaceRule()) + ReadBook.saveRead() + menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.getUseReplaceRule() + binding.readMenu.changeReplace(it.getUseReplaceRule()) + viewModel.replaceRuleChanged() + } + } + + private fun startBackupJob() { + backupJob?.cancel() + backupJob = lifecycleScope.launch(IO) { + delay(300000) + ReadBook.book?.let { + AppWebDav.uploadBookProgress(it) + ensureActive() + it.update() + Backup.autoBack(this@ReadBookActivity) + } + } + } + + override fun sureNewProgress(progress: BookProgress) { + syncDialog?.dismiss() + syncDialog = alert(R.string.get_book_progress) { + setMessage(R.string.cloud_progress_exceeds_current) + okButton { + ReadBook.setProgress(progress) + } + noButton() + } + } + + override fun onDestroy() { + super.onDestroy() + tts?.clearTts() + textActionMenu.dismiss() + popupAction.dismiss() + binding.readView.onDestroy() + ReadBook.unregister(this) + if (!ReadBook.inBookshelf && !isChangingConfigurations) { + viewModel.removeFromBookshelf(null) + } + if (!BuildConfig.DEBUG) { + Backup.autoBack(this) + } + } + + override fun observeLiveBus() = binding.run { + observeEvent(EventBus.TIME_CHANGED) { readView.upTime() } + observeEvent(EventBus.BATTERY_CHANGED) { readView.upBattery(it) } + observeEvent(EventBus.MEDIA_BUTTON) { + if (it) { + onClickReadAloud() + } else { + ReadBook.readAloud(!BaseReadAloudService.pause) + } + } + observeEvent>(EventBus.UP_CONFIG) { + it.forEach { value -> + when (value) { + 0 -> upSystemUiVisibility() + 1 -> readView.upBg() + 2 -> { + readView.upStyle() + binding.readMenu.reset() + } + 3 -> readView.upBgAlpha() + 4 -> readView.upPageSlopSquare() + 5 -> if (isInitFinish) ReadBook.loadContent(resetPageOffset = false) + 6 -> readView.upContent(resetPageOffset = false) + 8 -> ChapterProvider.upStyle() + 9 -> readView.invalidateTextPage() + 10 -> ChapterProvider.upLayout() + 11 -> readView.submitRenderTask() + } + } + } + observeEvent(EventBus.ALOUD_STATE) { + if (it == Status.STOP || it == Status.PAUSE) { + ReadBook.curTextChapter?.let { textChapter -> + val page = textChapter.getPageByReadPos(ReadBook.durChapterPos) + if (page != null) { + page.removePageAloudSpan() + readView.upContent(resetPageOffset = false) + } + } + } + } + observeEventSticky(EventBus.TTS_PROGRESS) { chapterStart -> + lifecycleScope.launch(IO) { + if (BaseReadAloudService.isPlay()) { + ReadBook.curTextChapter?.let { textChapter -> + val pageIndex = ReadBook.durPageIndex + val aloudSpanStart = chapterStart - textChapter.getReadLength(pageIndex) + textChapter.getPage(pageIndex) + ?.upPageAloudSpan(aloudSpanStart) + upContent() + } + } + } + } + observeEvent(PreferKey.keepLight) { + upScreenTimeOut() + } + observeEvent(PreferKey.textSelectAble) { + readView.curPage.upSelectAble(it) + } + observeEvent(PreferKey.showBrightnessView) { + readMenu.upBrightnessState() + } + observeEvent>(EventBus.SEARCH_RESULT) { + viewModel.searchResultList = it + } + observeEvent(EventBus.UPDATE_READ_ACTION_BAR) { + readMenu.reset() + } + observeEvent(EventBus.UP_SEEK_BAR) { + readMenu.upSeekBar() + } + observeEvent(EventBus.REFRESH_BOOK_CONTENT) { + ReadBook.book?.let { + ReadBook.curTextChapter = null + binding.readView.upContent() + viewModel.refreshContentDur(it) + } + } + } + + private fun upScreenTimeOut() { + val keepLightPrefer = getPrefString(PreferKey.keepLight)?.toInt() ?: 0 + screenTimeOut = keepLightPrefer * 1000L + screenOffTimerStart() + } + + /** + * 重置黑屏时间 + */ + override fun screenOffTimerStart() { + handler.post { + if (screenTimeOut < 0) { + keepScreenOn(true) + return@post + } + val t = screenTimeOut - sysScreenOffTime + if (t > 0) { + keepScreenOn(true) + handler.removeCallbacks(screenOffRunnable) + handler.postDelayed(screenOffRunnable, screenTimeOut) + } else { + keepScreenOn(false) + } + } + } + + override fun addToBookshelf(book: Book, toc: List) { + viewModel.addToBookshelf(book, toc) { + toastOnUi("已添加到书架") + } + } + + companion object { + const val RESULT_DELETED = 100 + } + +}