From 2a1a53cf34390afa517ee9bfa4e525fcfaa99d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Fri, 17 Jul 2026 21:51:40 +0300 Subject: [PATCH] fix: stabilize reader navigation and release 2.33 --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 1 + .../main/assets/reader_v2/js/epub-engine.js | 11 ++- .../main/assets/reader_v2/js/fb2-engine.js | 16 +++- app/src/main/assets/reader_v2/js/gestures.js | 50 ++++++++--- .../com/aletheia/app/ui/main/MainActivity.kt | 49 +++++++++-- .../aletheia/app/ui/reader/ReaderActivity.kt | 87 ++++++++++--------- app/src/main/res/layout/activity_reader.xml | 24 ++--- app/src/main/res/values/strings.xml | 1 + 9 files changed, 164 insertions(+), 79 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b6ede7..d7519c3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.aletheia.app" minSdk = 24 targetSdk = 36 - versionCode = 42 - versionName = "2.31" + versionCode = 44 + versionName = "2.33" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6041b09..2bbf8a8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ diff --git a/app/src/main/assets/reader_v2/js/epub-engine.js b/app/src/main/assets/reader_v2/js/epub-engine.js index 9d2e31d..1840245 100644 --- a/app/src/main/assets/reader_v2/js/epub-engine.js +++ b/app/src/main/assets/reader_v2/js/epub-engine.js @@ -21,6 +21,7 @@ this.preparedDocuments = new WeakSet(); this.resizeTimer = null; this.resizeObserver = null; + this.pendingResizeTarget = undefined; } async load(arrayBuffer, payload) { @@ -139,7 +140,8 @@ getPreferences: this.host.getPreferences, next: this.host.next, previous: this.host.previous, - toggleControls: this.host.toggleControls + toggleControls: this.host.toggleControls, + nativeTapZones: true }); if (!this.preparedDocuments.has(contents.document)) { this.preparedDocuments.add(contents.document); @@ -253,10 +255,14 @@ if (!global.ResizeObserver || this.resizeObserver) return; const self = this; this.resizeObserver = new ResizeObserver(function () { + if (self.pendingResizeTarget === undefined) { + self.pendingResizeTarget = self.currentCfi || null; + } clearTimeout(self.resizeTimer); self.resizeTimer = setTimeout(async function () { if (!self.rendition || self.destroyed) return; - const target = self.currentCfi; + const target = self.pendingResizeTarget; + self.pendingResizeTarget = undefined; try { self.rendition.resize(); if (target) await self.rendition.display(target); @@ -594,6 +600,7 @@ destroy() { this.destroyed = true; clearTimeout(this.resizeTimer); + this.pendingResizeTarget = undefined; if (this.resizeObserver) this.resizeObserver.disconnect(); this.resizeObserver = null; this.clearSearch(false); diff --git a/app/src/main/assets/reader_v2/js/fb2-engine.js b/app/src/main/assets/reader_v2/js/fb2-engine.js index 750a08a..15f55cf 100644 --- a/app/src/main/assets/reader_v2/js/fb2-engine.js +++ b/app/src/main/assets/reader_v2/js/fb2-engine.js @@ -146,6 +146,7 @@ this.searchMarks = []; this.resizeObserver = null; this.resizeTimer = null; + this.pendingResizeLocator = undefined; this.scrollFrame = 0; this.selectionTimer = null; this.detachGestures = null; @@ -169,7 +170,8 @@ getPreferences: this.host.getPreferences, next: this.host.next, previous: this.host.previous, - toggleControls: this.host.toggleControls + toggleControls: this.host.toggleControls, + nativeTapZones: true }); await Internal.waitForPreferredFont(document, this.host.getPreferences()); await this._layout(false); @@ -487,9 +489,9 @@ return wrapper; } - async _layout(preserve) { + async _layout(preserve, preservedLocator) { const preferences = this.host.getPreferences(); - const locator = preserve ? this.currentLocator() : null; + const locator = preserve ? (preservedLocator || this.currentLocator()) : null; const progress = this.currentProgress; const width = Math.max(1, this.mount.clientWidth); const height = Math.max(1, this.mount.clientHeight); @@ -534,9 +536,14 @@ if (!global.ResizeObserver || this.resizeObserver) return; const self = this; this.resizeObserver = new ResizeObserver(function () { + if (self.pendingResizeLocator === undefined) { + self.pendingResizeLocator = self.currentLocator(); + } clearTimeout(self.resizeTimer); self.resizeTimer = setTimeout(function () { - if (!self.destroyed) self._layout(true).catch(function (error) { + const locator = self.pendingResizeLocator; + self.pendingResizeLocator = undefined; + if (!self.destroyed) self._layout(true, locator).catch(function (error) { Internal.reportError(error, 'fb2.resize', true); }); }, 120); @@ -1051,6 +1058,7 @@ destroy() { this.destroyed = true; clearTimeout(this.resizeTimer); + this.pendingResizeLocator = undefined; clearTimeout(this.selectionTimer); if (this.scrollFrame) cancelAnimationFrame(this.scrollFrame); if (this.resizeObserver) this.resizeObserver.disconnect(); diff --git a/app/src/main/assets/reader_v2/js/gestures.js b/app/src/main/assets/reader_v2/js/gestures.js index 17f5377..047dcef 100644 --- a/app/src/main/assets/reader_v2/js/gestures.js +++ b/app/src/main/assets/reader_v2/js/gestures.js @@ -3,6 +3,17 @@ const Internal = global.ReaderV2Internal = global.ReaderV2Internal || {}; const attachedDocuments = new WeakMap(); + const LEFT_ZONE_END = 1 / 3; + const RIGHT_ZONE_START = 2 / 3; + let lastHandledTouchAt = 0; + + function viewportWidth(doc) { + const viewWidth = Number(doc && doc.defaultView && doc.defaultView.innerWidth); + if (Number.isFinite(viewWidth) && viewWidth > 0) return viewWidth; + const documentWidth = Number(doc && doc.documentElement && doc.documentElement.clientWidth); + if (Number.isFinite(documentWidth) && documentWidth > 0) return documentWidth; + return Math.max(1, Number(global.innerWidth) || 1); + } function selectionText(doc) { try { @@ -39,7 +50,7 @@ startAt = Date.now(); startTarget = event.target; moved = false; - const width = doc.documentElement.clientWidth || global.innerWidth; + const width = viewportWidth(doc); brightnessCandidate = Boolean(preferences().brightnessGesture && startX <= width * .18); } @@ -55,16 +66,18 @@ } function doTap(x, width) { + if (options.nativeTapZones) return false; const current = preferences(); const mode = current.pageTurnMode || 'tapSwipe'; const normalized = x / Math.max(1, width); - if (normalized >= .28 && normalized <= .72) { + if (normalized >= LEFT_ZONE_END && normalized <= RIGHT_ZONE_START) { options.toggleControls(); - return; + return true; } - if (mode !== 'tapSwipe' && mode !== 'tap') return; - if (normalized < .28) current.invertZones ? options.next() : options.previous(); + if (mode !== 'tapSwipe' && mode !== 'tap') return false; + if (normalized < LEFT_ZONE_END) current.invertZones ? options.next() : options.previous(); else current.invertZones ? options.previous() : options.next(); + return true; } function onTouchEnd(event) { @@ -76,14 +89,14 @@ const horizontal = Math.abs(dx) > Math.abs(dy) * 1.25; const current = preferences(); const mode = current.pageTurnMode || 'tapSwipe'; - const viewportWidth = doc.documentElement.clientWidth || global.innerWidth; + const width = viewportWidth(doc); const viewportHeight = doc.documentElement.clientHeight || global.innerHeight; if (interactiveTarget(startTarget) || selectionText(doc)) return; if ( current.brightnessGesture && - startX <= viewportWidth * .18 && + startX <= width * .18 && elapsed < 1200 && Math.abs(dy) >= 28 && Math.abs(dy) > Math.abs(dx) * 1.35 @@ -92,24 +105,37 @@ delta: Internal.clamp((-dy / Math.max(1, viewportHeight)) * 100, -100, 100) }); consumedAt = Date.now(); + lastHandledTouchAt = consumedAt; + event.preventDefault(); return; } if (elapsed < 650 && horizontal && Math.abs(dx) >= 48 && (mode === 'tapSwipe' || mode === 'swipe')) { dx < 0 ? options.next() : options.previous(); consumedAt = Date.now(); + lastHandledTouchAt = consumedAt; + event.preventDefault(); return; } if (elapsed < 300 && !moved && Math.abs(dx) < 12 && Math.abs(dy) < 12) { - doTap(touch.clientX, doc.documentElement.clientWidth || global.innerWidth); - consumedAt = Date.now(); + if (doTap(touch.clientX, width)) { + consumedAt = Date.now(); + lastHandledTouchAt = consumedAt; + event.preventDefault(); + } } } function onClick(event) { - if (Date.now() - consumedAt < 500 || interactiveTarget(event.target) || selectionText(doc)) return; - doTap(event.clientX, doc.documentElement.clientWidth || global.innerWidth); + const now = Date.now(); + if ( + now - consumedAt < 700 || + now - lastHandledTouchAt < 700 || + interactiveTarget(event.target) || + selectionText(doc) + ) return; + doTap(event.clientX, viewportWidth(doc)); } function onKeyDown(event) { @@ -127,7 +153,7 @@ doc.addEventListener('touchstart', onTouchStart, { passive: true }); doc.addEventListener('touchmove', onTouchMove, { passive: false }); - doc.addEventListener('touchend', onTouchEnd, { passive: true }); + doc.addEventListener('touchend', onTouchEnd, { passive: false }); doc.addEventListener('click', onClick, false); doc.addEventListener('keydown', onKeyDown, false); diff --git a/app/src/main/java/com/aletheia/app/ui/main/MainActivity.kt b/app/src/main/java/com/aletheia/app/ui/main/MainActivity.kt index 7d39040..9e237ed 100644 --- a/app/src/main/java/com/aletheia/app/ui/main/MainActivity.kt +++ b/app/src/main/java/com/aletheia/app/ui/main/MainActivity.kt @@ -5,6 +5,7 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat @@ -24,9 +25,9 @@ import com.aletheia.app.ui.books.BookshelfFragment import com.aletheia.app.ui.home.HomeFragment import com.aletheia.app.ui.qbooks.QBooksLibraryFragment import com.aletheia.app.ui.reader.ReaderActivity -import com.aletheia.app.ui.reader.ReaderHubFragment import com.aletheia.app.ui.settings.SettingsFragment import com.aletheia.app.util.setBookCover +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,7 +58,10 @@ class MainActivity : AppCompatActivity() { if (savedInstanceState == null) { selectOrOpenTab(resolveInitialTab(intent?.getStringExtra(EXTRA_OPEN_TAB))) - } else if (supportFragmentManager.fragments.none { !it.isHidden }) { + } else if ( + supportFragmentManager.fragments.none { !it.isHidden } || + supportFragmentManager.findFragmentByTag(LEGACY_TAG_READER)?.isHidden == false + ) { selectOrOpenTab(R.id.nav_home) } } @@ -86,7 +90,10 @@ class MainActivity : AppCompatActivity() { private fun openTab(itemId: Int): Boolean = when (itemId) { R.id.nav_home -> showTab(TAG_HOME) { HomeFragment.newInstance() } R.id.nav_search -> showTab(TAG_SEARCH) { QBooksLibraryFragment.newInstance() } - R.id.nav_reader -> showTab(TAG_READER) { ReaderHubFragment.newInstance() } + R.id.nav_reader -> { + openLastReadBook() + false + } R.id.nav_my_books -> showTab(TAG_MY_BOOKS) { BookshelfFragment.newInstance() } R.id.nav_profile -> showTab(TAG_PROFILE) { SettingsFragment.newInstance() } else -> false @@ -130,9 +137,7 @@ class MainActivity : AppCompatActivity() { private fun refreshMiniReader() { lifecycleScope.launch { - val book = withContext(Dispatchers.IO) { - app.bookRepository.getAllBooks().maxByOrNull { item -> item.lastRead } - } + val book = findLastReadableBook() miniReaderBook = book val visibility = if (book == null) View.GONE else View.VISIBLE binding.miniReaderPanel.visibility = visibility @@ -150,6 +155,36 @@ class MainActivity : AppCompatActivity() { private fun openMiniReader() { val book = miniReaderBook ?: return + openReader(book) + } + + private fun openLastReadBook() { + if (supportFragmentManager.fragments.none { fragment -> !fragment.isHidden }) { + showTab(TAG_HOME) { HomeFragment.newInstance() } + } + lifecycleScope.launch { + val book = findLastReadableBook() + if (book == null) { + Toast.makeText( + this@MainActivity, + R.string.reader_no_book_selected, + Toast.LENGTH_SHORT + ).show() + return@launch + } + miniReaderBook = book + openReader(book) + } + } + + private suspend fun findLastReadableBook(): Book? = withContext(Dispatchers.IO) { + app.bookRepository.getAllBooks() + .asSequence() + .filter { book -> File(book.filePath).isFile } + .maxByOrNull { book -> book.lastRead } + } + + private fun openReader(book: Book) { startActivity( Intent(this, ReaderActivity::class.java) .putExtra(ReaderActivity.EXTRA_BOOK_ID, book.id) @@ -203,7 +238,7 @@ class MainActivity : AppCompatActivity() { const val EXTRA_OPEN_TAB = "open_tab" private const val TAG_HOME = "home" private const val TAG_SEARCH = "search" - private const val TAG_READER = "reader" + private const val LEGACY_TAG_READER = "reader" private const val TAG_MY_BOOKS = "my-books" private const val TAG_PROFILE = "profile" } diff --git a/app/src/main/java/com/aletheia/app/ui/reader/ReaderActivity.kt b/app/src/main/java/com/aletheia/app/ui/reader/ReaderActivity.kt index 00d9a65..b6f49e7 100644 --- a/app/src/main/java/com/aletheia/app/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/com/aletheia/app/ui/reader/ReaderActivity.kt @@ -5,6 +5,7 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo +import android.content.res.Configuration import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -14,7 +15,6 @@ import android.graphics.drawable.StateListDrawable import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.SystemClock import android.util.Log import android.view.KeyEvent import android.view.Gravity @@ -23,6 +23,7 @@ import android.view.View import android.view.ViewConfiguration import android.view.ViewGroup import android.view.WindowManager +import android.webkit.WebView import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView @@ -99,11 +100,9 @@ class ReaderActivity : AppCompatActivity() { private var readerTouchDownX = 0f private var readerTouchDownY = 0f private var readerTouchDownAt = 0L - private var lastNativeControlsToggleAt = 0L private val readerTapSlop by lazy(LazyThreadSafetyMode.NONE) { ViewConfiguration.get(this).scaledTouchSlop.toFloat() } - private var pageHeaderBasePadding = Insets() private var controlsBasePadding = Insets() private var settingsBasePadding = Insets() @@ -159,6 +158,12 @@ class ReaderActivity : AppCompatActivity() { applySystemUi() } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ViewCompat.requestApplyInsets(binding.root) + applySystemUi() + } + override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(STATE_FINISHING, finishingReader) outState.putString(STATE_PREFERENCES, preferences.toStateJson()) @@ -196,11 +201,11 @@ class ReaderActivity : AppCompatActivity() { if (bookReady && preferences.volumeButtons && !preferences.verticalScroll) { when (keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN -> { - webController.next() + webController.previous() return true } KeyEvent.KEYCODE_VOLUME_UP -> { - webController.previous() + webController.next() return true } } @@ -546,8 +551,8 @@ class ReaderActivity : AppCompatActivity() { currentPage = book.currentPage.coerceAtLeast(1), totalPages = book.totalPages.coerceAtLeast(1) ) - position = app.readerStateCoordinator.latest(book.id)?.toReaderPosition() - ?: restoredPosition + position = restoredPosition + ?: app.readerStateCoordinator.latest(book.id)?.toReaderPosition() ?: databasePosition restoredPosition = null publishPosition() @@ -589,10 +594,7 @@ class ReaderActivity : AppCompatActivity() { if (finishingReader) return when (event) { ReaderEvent.ShellReady -> binding.readerLoadingText.text = "Открываю книгу…" - ReaderEvent.ToggleControls -> { - val sinceNativeToggle = SystemClock.uptimeMillis() - lastNativeControlsToggleAt - if (sinceNativeToggle > NATIVE_TAP_DUPLICATE_WINDOW_MS) toggleControls() - } + ReaderEvent.ToggleControls -> toggleControls() ReaderEvent.Navigation -> Unit is ReaderEvent.BookReady -> { bookReady = true @@ -669,6 +671,14 @@ class ReaderActivity : AppCompatActivity() { } } + private fun toggleControls() { + if (binding.readerSettingsOverlay.isVisible || binding.readerContentsOverlay.isVisible || + binding.readerSelectionOverlay.isVisible || binding.noteEditorOverlay.isVisible + ) return + binding.readerControlsOverlay.isVisible = !binding.readerControlsOverlay.isVisible + renderChromeVisibility() + } + private fun observeNativeReaderTap(event: MotionEvent) { when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { @@ -694,38 +704,46 @@ class ReaderActivity : AppCompatActivity() { } MotionEvent.ACTION_UP -> { val elapsed = event.eventTime - readerTouchDownAt - val normalizedX = event.x / binding.readerWebView.width.coerceAtLeast(1) val modalVisible = binding.readerLoadingOverlay.isVisible || binding.readerSettingsOverlay.isVisible || binding.readerContentsOverlay.isVisible || binding.readerSelectionOverlay.isVisible || binding.noteEditorOverlay.isVisible - val shouldToggle = readerTouchActive && + val hitType = binding.readerWebView.hitTestResult.type + val interactiveTarget = hitType == WebView.HitTestResult.SRC_ANCHOR_TYPE || + hitType == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || + hitType == WebView.HitTestResult.EDIT_TEXT_TYPE || + hitType == WebView.HitTestResult.EMAIL_TYPE || + hitType == WebView.HitTestResult.PHONE_TYPE || + hitType == WebView.HitTestResult.GEO_TYPE + val shouldHandle = readerTouchActive && !readerTouchMoved && elapsed in 0..NATIVE_TAP_MAX_DURATION_MS && - normalizedX in READER_CENTER_ZONE_START..READER_CENTER_ZONE_END && bookReady && !finishingReader && currentSelection == null && - !modalVisible + !modalVisible && + !interactiveTarget readerTouchActive = false readerTouchMoved = false - if (shouldToggle) { - lastNativeControlsToggleAt = SystemClock.uptimeMillis() - toggleControls() + if (!shouldHandle) return + + val normalizedX = event.x / binding.readerWebView.width.coerceAtLeast(1) + when { + normalizedX in READER_CENTER_ZONE_START..READER_CENTER_ZONE_END -> toggleControls() + preferences.pageTurnMode != ReaderPreferences.PAGE_TURN_TAP && + preferences.pageTurnMode != ReaderPreferences.PAGE_TURN_TAP_SWIPE -> Unit + normalizedX < READER_CENTER_ZONE_START -> { + if (preferences.invertZones) webController.next() else webController.previous() + } + else -> { + if (preferences.invertZones) webController.previous() else webController.next() + } } } } } - private fun toggleControls() { - if (binding.readerSettingsOverlay.isVisible || binding.readerContentsOverlay.isVisible || - binding.readerSelectionOverlay.isVisible || binding.noteEditorOverlay.isVisible - ) return - binding.readerControlsOverlay.isVisible = !binding.readerControlsOverlay.isVisible - renderChromeVisibility() - } - private fun renderChromeVisibility() { binding.pageHeader.isVisible = preferences.showTitle && !binding.readerControlsOverlay.isVisible && @@ -1523,17 +1541,7 @@ class ReaderActivity : AppCompatActivity() { } private fun handleBack() { - when { - binding.noteEditorOverlay.isVisible -> closeNoteEditor() - binding.readerSelectionOverlay.isVisible -> closeSelection() - binding.readerContentsOverlay.isVisible -> closeContents() - binding.readerSettingsOverlay.isVisible -> closeSettings() - binding.readerControlsOverlay.isVisible -> { - binding.readerControlsOverlay.isVisible = false - renderChromeVisibility() - } - else -> finishReader() - } + finishReader() } private fun openExternalLink(url: String) { @@ -1719,9 +1727,8 @@ class ReaderActivity : AppCompatActivity() { private const val PROGRESS_SAVE_DEBOUNCE_MS = 1_500L private const val SETTINGS_SAVE_DEBOUNCE_MS = 180L private const val NATIVE_TAP_MAX_DURATION_MS = 350L - private const val NATIVE_TAP_DUPLICATE_WINDOW_MS = 450L - private const val READER_CENTER_ZONE_START = 0.28f - private const val READER_CENTER_ZONE_END = 0.72f + private const val READER_CENTER_ZONE_START = 1f / 3f + private const val READER_CENTER_ZONE_END = 2f / 3f private val FATAL_ERROR_STAGES = setOf( "book.load", "api.loadBook", diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml index bbc17b1..86131a8 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -629,13 +629,13 @@ android:focusable="true"> @@ -650,13 +650,13 @@ android:focusable="true"> @@ -832,13 +832,13 @@ android:focusable="true"> @@ -853,13 +853,13 @@ android:focusable="true"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6851a9c..c793993 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + Книга для чтения не выбрана Алетея Мои книги