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 @@
+ Книга для чтения не выбрана
Алетея
Мои книги