fix: stabilize reader navigation and release 2.33
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.aletheia.app"
|
applicationId = "com.aletheia.app"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 42
|
versionCode = 44
|
||||||
versionName = "2.31"
|
versionName = "2.33"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.Aletheia"
|
android:theme="@style/Theme.Aletheia"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
this.preparedDocuments = new WeakSet();
|
this.preparedDocuments = new WeakSet();
|
||||||
this.resizeTimer = null;
|
this.resizeTimer = null;
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
|
this.pendingResizeTarget = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(arrayBuffer, payload) {
|
async load(arrayBuffer, payload) {
|
||||||
@@ -139,7 +140,8 @@
|
|||||||
getPreferences: this.host.getPreferences,
|
getPreferences: this.host.getPreferences,
|
||||||
next: this.host.next,
|
next: this.host.next,
|
||||||
previous: this.host.previous,
|
previous: this.host.previous,
|
||||||
toggleControls: this.host.toggleControls
|
toggleControls: this.host.toggleControls,
|
||||||
|
nativeTapZones: true
|
||||||
});
|
});
|
||||||
if (!this.preparedDocuments.has(contents.document)) {
|
if (!this.preparedDocuments.has(contents.document)) {
|
||||||
this.preparedDocuments.add(contents.document);
|
this.preparedDocuments.add(contents.document);
|
||||||
@@ -253,10 +255,14 @@
|
|||||||
if (!global.ResizeObserver || this.resizeObserver) return;
|
if (!global.ResizeObserver || this.resizeObserver) return;
|
||||||
const self = this;
|
const self = this;
|
||||||
this.resizeObserver = new ResizeObserver(function () {
|
this.resizeObserver = new ResizeObserver(function () {
|
||||||
|
if (self.pendingResizeTarget === undefined) {
|
||||||
|
self.pendingResizeTarget = self.currentCfi || null;
|
||||||
|
}
|
||||||
clearTimeout(self.resizeTimer);
|
clearTimeout(self.resizeTimer);
|
||||||
self.resizeTimer = setTimeout(async function () {
|
self.resizeTimer = setTimeout(async function () {
|
||||||
if (!self.rendition || self.destroyed) return;
|
if (!self.rendition || self.destroyed) return;
|
||||||
const target = self.currentCfi;
|
const target = self.pendingResizeTarget;
|
||||||
|
self.pendingResizeTarget = undefined;
|
||||||
try {
|
try {
|
||||||
self.rendition.resize();
|
self.rendition.resize();
|
||||||
if (target) await self.rendition.display(target);
|
if (target) await self.rendition.display(target);
|
||||||
@@ -594,6 +600,7 @@
|
|||||||
destroy() {
|
destroy() {
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
clearTimeout(this.resizeTimer);
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.pendingResizeTarget = undefined;
|
||||||
if (this.resizeObserver) this.resizeObserver.disconnect();
|
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
this.clearSearch(false);
|
this.clearSearch(false);
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
this.searchMarks = [];
|
this.searchMarks = [];
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
this.resizeTimer = null;
|
this.resizeTimer = null;
|
||||||
|
this.pendingResizeLocator = undefined;
|
||||||
this.scrollFrame = 0;
|
this.scrollFrame = 0;
|
||||||
this.selectionTimer = null;
|
this.selectionTimer = null;
|
||||||
this.detachGestures = null;
|
this.detachGestures = null;
|
||||||
@@ -169,7 +170,8 @@
|
|||||||
getPreferences: this.host.getPreferences,
|
getPreferences: this.host.getPreferences,
|
||||||
next: this.host.next,
|
next: this.host.next,
|
||||||
previous: this.host.previous,
|
previous: this.host.previous,
|
||||||
toggleControls: this.host.toggleControls
|
toggleControls: this.host.toggleControls,
|
||||||
|
nativeTapZones: true
|
||||||
});
|
});
|
||||||
await Internal.waitForPreferredFont(document, this.host.getPreferences());
|
await Internal.waitForPreferredFont(document, this.host.getPreferences());
|
||||||
await this._layout(false);
|
await this._layout(false);
|
||||||
@@ -487,9 +489,9 @@
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _layout(preserve) {
|
async _layout(preserve, preservedLocator) {
|
||||||
const preferences = this.host.getPreferences();
|
const preferences = this.host.getPreferences();
|
||||||
const locator = preserve ? this.currentLocator() : null;
|
const locator = preserve ? (preservedLocator || this.currentLocator()) : null;
|
||||||
const progress = this.currentProgress;
|
const progress = this.currentProgress;
|
||||||
const width = Math.max(1, this.mount.clientWidth);
|
const width = Math.max(1, this.mount.clientWidth);
|
||||||
const height = Math.max(1, this.mount.clientHeight);
|
const height = Math.max(1, this.mount.clientHeight);
|
||||||
@@ -534,9 +536,14 @@
|
|||||||
if (!global.ResizeObserver || this.resizeObserver) return;
|
if (!global.ResizeObserver || this.resizeObserver) return;
|
||||||
const self = this;
|
const self = this;
|
||||||
this.resizeObserver = new ResizeObserver(function () {
|
this.resizeObserver = new ResizeObserver(function () {
|
||||||
|
if (self.pendingResizeLocator === undefined) {
|
||||||
|
self.pendingResizeLocator = self.currentLocator();
|
||||||
|
}
|
||||||
clearTimeout(self.resizeTimer);
|
clearTimeout(self.resizeTimer);
|
||||||
self.resizeTimer = setTimeout(function () {
|
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);
|
Internal.reportError(error, 'fb2.resize', true);
|
||||||
});
|
});
|
||||||
}, 120);
|
}, 120);
|
||||||
@@ -1051,6 +1058,7 @@
|
|||||||
destroy() {
|
destroy() {
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
clearTimeout(this.resizeTimer);
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.pendingResizeLocator = undefined;
|
||||||
clearTimeout(this.selectionTimer);
|
clearTimeout(this.selectionTimer);
|
||||||
if (this.scrollFrame) cancelAnimationFrame(this.scrollFrame);
|
if (this.scrollFrame) cancelAnimationFrame(this.scrollFrame);
|
||||||
if (this.resizeObserver) this.resizeObserver.disconnect();
|
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
|
|
||||||
const Internal = global.ReaderV2Internal = global.ReaderV2Internal || {};
|
const Internal = global.ReaderV2Internal = global.ReaderV2Internal || {};
|
||||||
const attachedDocuments = new WeakMap();
|
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) {
|
function selectionText(doc) {
|
||||||
try {
|
try {
|
||||||
@@ -39,7 +50,7 @@
|
|||||||
startAt = Date.now();
|
startAt = Date.now();
|
||||||
startTarget = event.target;
|
startTarget = event.target;
|
||||||
moved = false;
|
moved = false;
|
||||||
const width = doc.documentElement.clientWidth || global.innerWidth;
|
const width = viewportWidth(doc);
|
||||||
brightnessCandidate = Boolean(preferences().brightnessGesture && startX <= width * .18);
|
brightnessCandidate = Boolean(preferences().brightnessGesture && startX <= width * .18);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,16 +66,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doTap(x, width) {
|
function doTap(x, width) {
|
||||||
|
if (options.nativeTapZones) return false;
|
||||||
const current = preferences();
|
const current = preferences();
|
||||||
const mode = current.pageTurnMode || 'tapSwipe';
|
const mode = current.pageTurnMode || 'tapSwipe';
|
||||||
const normalized = x / Math.max(1, width);
|
const normalized = x / Math.max(1, width);
|
||||||
if (normalized >= .28 && normalized <= .72) {
|
if (normalized >= LEFT_ZONE_END && normalized <= RIGHT_ZONE_START) {
|
||||||
options.toggleControls();
|
options.toggleControls();
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
if (mode !== 'tapSwipe' && mode !== 'tap') return;
|
if (mode !== 'tapSwipe' && mode !== 'tap') return false;
|
||||||
if (normalized < .28) current.invertZones ? options.next() : options.previous();
|
if (normalized < LEFT_ZONE_END) current.invertZones ? options.next() : options.previous();
|
||||||
else current.invertZones ? options.previous() : options.next();
|
else current.invertZones ? options.previous() : options.next();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchEnd(event) {
|
function onTouchEnd(event) {
|
||||||
@@ -76,14 +89,14 @@
|
|||||||
const horizontal = Math.abs(dx) > Math.abs(dy) * 1.25;
|
const horizontal = Math.abs(dx) > Math.abs(dy) * 1.25;
|
||||||
const current = preferences();
|
const current = preferences();
|
||||||
const mode = current.pageTurnMode || 'tapSwipe';
|
const mode = current.pageTurnMode || 'tapSwipe';
|
||||||
const viewportWidth = doc.documentElement.clientWidth || global.innerWidth;
|
const width = viewportWidth(doc);
|
||||||
const viewportHeight = doc.documentElement.clientHeight || global.innerHeight;
|
const viewportHeight = doc.documentElement.clientHeight || global.innerHeight;
|
||||||
|
|
||||||
if (interactiveTarget(startTarget) || selectionText(doc)) return;
|
if (interactiveTarget(startTarget) || selectionText(doc)) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
current.brightnessGesture &&
|
current.brightnessGesture &&
|
||||||
startX <= viewportWidth * .18 &&
|
startX <= width * .18 &&
|
||||||
elapsed < 1200 &&
|
elapsed < 1200 &&
|
||||||
Math.abs(dy) >= 28 &&
|
Math.abs(dy) >= 28 &&
|
||||||
Math.abs(dy) > Math.abs(dx) * 1.35
|
Math.abs(dy) > Math.abs(dx) * 1.35
|
||||||
@@ -92,24 +105,37 @@
|
|||||||
delta: Internal.clamp((-dy / Math.max(1, viewportHeight)) * 100, -100, 100)
|
delta: Internal.clamp((-dy / Math.max(1, viewportHeight)) * 100, -100, 100)
|
||||||
});
|
});
|
||||||
consumedAt = Date.now();
|
consumedAt = Date.now();
|
||||||
|
lastHandledTouchAt = consumedAt;
|
||||||
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elapsed < 650 && horizontal && Math.abs(dx) >= 48 && (mode === 'tapSwipe' || mode === 'swipe')) {
|
if (elapsed < 650 && horizontal && Math.abs(dx) >= 48 && (mode === 'tapSwipe' || mode === 'swipe')) {
|
||||||
dx < 0 ? options.next() : options.previous();
|
dx < 0 ? options.next() : options.previous();
|
||||||
consumedAt = Date.now();
|
consumedAt = Date.now();
|
||||||
|
lastHandledTouchAt = consumedAt;
|
||||||
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elapsed < 300 && !moved && Math.abs(dx) < 12 && Math.abs(dy) < 12) {
|
if (elapsed < 300 && !moved && Math.abs(dx) < 12 && Math.abs(dy) < 12) {
|
||||||
doTap(touch.clientX, doc.documentElement.clientWidth || global.innerWidth);
|
if (doTap(touch.clientX, width)) {
|
||||||
consumedAt = Date.now();
|
consumedAt = Date.now();
|
||||||
|
lastHandledTouchAt = consumedAt;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(event) {
|
function onClick(event) {
|
||||||
if (Date.now() - consumedAt < 500 || interactiveTarget(event.target) || selectionText(doc)) return;
|
const now = Date.now();
|
||||||
doTap(event.clientX, doc.documentElement.clientWidth || global.innerWidth);
|
if (
|
||||||
|
now - consumedAt < 700 ||
|
||||||
|
now - lastHandledTouchAt < 700 ||
|
||||||
|
interactiveTarget(event.target) ||
|
||||||
|
selectionText(doc)
|
||||||
|
) return;
|
||||||
|
doTap(event.clientX, viewportWidth(doc));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(event) {
|
function onKeyDown(event) {
|
||||||
@@ -127,7 +153,7 @@
|
|||||||
|
|
||||||
doc.addEventListener('touchstart', onTouchStart, { passive: true });
|
doc.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||||
doc.addEventListener('touchmove', onTouchMove, { passive: false });
|
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('click', onClick, false);
|
||||||
doc.addEventListener('keydown', onKeyDown, false);
|
doc.addEventListener('keydown', onKeyDown, false);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.ViewCompat
|
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.home.HomeFragment
|
||||||
import com.aletheia.app.ui.qbooks.QBooksLibraryFragment
|
import com.aletheia.app.ui.qbooks.QBooksLibraryFragment
|
||||||
import com.aletheia.app.ui.reader.ReaderActivity
|
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.ui.settings.SettingsFragment
|
||||||
import com.aletheia.app.util.setBookCover
|
import com.aletheia.app.util.setBookCover
|
||||||
|
import java.io.File
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -57,7 +58,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
selectOrOpenTab(resolveInitialTab(intent?.getStringExtra(EXTRA_OPEN_TAB)))
|
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)
|
selectOrOpenTab(R.id.nav_home)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +90,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private fun openTab(itemId: Int): Boolean = when (itemId) {
|
private fun openTab(itemId: Int): Boolean = when (itemId) {
|
||||||
R.id.nav_home -> showTab(TAG_HOME) { HomeFragment.newInstance() }
|
R.id.nav_home -> showTab(TAG_HOME) { HomeFragment.newInstance() }
|
||||||
R.id.nav_search -> showTab(TAG_SEARCH) { QBooksLibraryFragment.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_my_books -> showTab(TAG_MY_BOOKS) { BookshelfFragment.newInstance() }
|
||||||
R.id.nav_profile -> showTab(TAG_PROFILE) { SettingsFragment.newInstance() }
|
R.id.nav_profile -> showTab(TAG_PROFILE) { SettingsFragment.newInstance() }
|
||||||
else -> false
|
else -> false
|
||||||
@@ -130,9 +137,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun refreshMiniReader() {
|
private fun refreshMiniReader() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val book = withContext(Dispatchers.IO) {
|
val book = findLastReadableBook()
|
||||||
app.bookRepository.getAllBooks().maxByOrNull { item -> item.lastRead }
|
|
||||||
}
|
|
||||||
miniReaderBook = book
|
miniReaderBook = book
|
||||||
val visibility = if (book == null) View.GONE else View.VISIBLE
|
val visibility = if (book == null) View.GONE else View.VISIBLE
|
||||||
binding.miniReaderPanel.visibility = visibility
|
binding.miniReaderPanel.visibility = visibility
|
||||||
@@ -150,6 +155,36 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun openMiniReader() {
|
private fun openMiniReader() {
|
||||||
val book = miniReaderBook ?: return
|
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(
|
startActivity(
|
||||||
Intent(this, ReaderActivity::class.java)
|
Intent(this, ReaderActivity::class.java)
|
||||||
.putExtra(ReaderActivity.EXTRA_BOOK_ID, book.id)
|
.putExtra(ReaderActivity.EXTRA_BOOK_ID, book.id)
|
||||||
@@ -203,7 +238,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
const val EXTRA_OPEN_TAB = "open_tab"
|
const val EXTRA_OPEN_TAB = "open_tab"
|
||||||
private const val TAG_HOME = "home"
|
private const val TAG_HOME = "home"
|
||||||
private const val TAG_SEARCH = "search"
|
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_MY_BOOKS = "my-books"
|
||||||
private const val TAG_PROFILE = "profile"
|
private const val TAG_PROFILE = "profile"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.ClipboardManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
@@ -14,7 +15,6 @@ import android.graphics.drawable.StateListDrawable
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
@@ -23,6 +23,7 @@ import android.view.View
|
|||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.webkit.WebView
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -99,11 +100,9 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
private var readerTouchDownX = 0f
|
private var readerTouchDownX = 0f
|
||||||
private var readerTouchDownY = 0f
|
private var readerTouchDownY = 0f
|
||||||
private var readerTouchDownAt = 0L
|
private var readerTouchDownAt = 0L
|
||||||
private var lastNativeControlsToggleAt = 0L
|
|
||||||
private val readerTapSlop by lazy(LazyThreadSafetyMode.NONE) {
|
private val readerTapSlop by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
ViewConfiguration.get(this).scaledTouchSlop.toFloat()
|
ViewConfiguration.get(this).scaledTouchSlop.toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var pageHeaderBasePadding = Insets()
|
private var pageHeaderBasePadding = Insets()
|
||||||
private var controlsBasePadding = Insets()
|
private var controlsBasePadding = Insets()
|
||||||
private var settingsBasePadding = Insets()
|
private var settingsBasePadding = Insets()
|
||||||
@@ -159,6 +158,12 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
applySystemUi()
|
applySystemUi()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
ViewCompat.requestApplyInsets(binding.root)
|
||||||
|
applySystemUi()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean(STATE_FINISHING, finishingReader)
|
outState.putBoolean(STATE_FINISHING, finishingReader)
|
||||||
outState.putString(STATE_PREFERENCES, preferences.toStateJson())
|
outState.putString(STATE_PREFERENCES, preferences.toStateJson())
|
||||||
@@ -196,11 +201,11 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
if (bookReady && preferences.volumeButtons && !preferences.verticalScroll) {
|
if (bookReady && preferences.volumeButtons && !preferences.verticalScroll) {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
webController.next()
|
webController.previous()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
webController.previous()
|
webController.next()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,8 +551,8 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
currentPage = book.currentPage.coerceAtLeast(1),
|
currentPage = book.currentPage.coerceAtLeast(1),
|
||||||
totalPages = book.totalPages.coerceAtLeast(1)
|
totalPages = book.totalPages.coerceAtLeast(1)
|
||||||
)
|
)
|
||||||
position = app.readerStateCoordinator.latest(book.id)?.toReaderPosition()
|
position = restoredPosition
|
||||||
?: restoredPosition
|
?: app.readerStateCoordinator.latest(book.id)?.toReaderPosition()
|
||||||
?: databasePosition
|
?: databasePosition
|
||||||
restoredPosition = null
|
restoredPosition = null
|
||||||
publishPosition()
|
publishPosition()
|
||||||
@@ -589,10 +594,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
if (finishingReader) return
|
if (finishingReader) return
|
||||||
when (event) {
|
when (event) {
|
||||||
ReaderEvent.ShellReady -> binding.readerLoadingText.text = "Открываю книгу…"
|
ReaderEvent.ShellReady -> binding.readerLoadingText.text = "Открываю книгу…"
|
||||||
ReaderEvent.ToggleControls -> {
|
ReaderEvent.ToggleControls -> toggleControls()
|
||||||
val sinceNativeToggle = SystemClock.uptimeMillis() - lastNativeControlsToggleAt
|
|
||||||
if (sinceNativeToggle > NATIVE_TAP_DUPLICATE_WINDOW_MS) toggleControls()
|
|
||||||
}
|
|
||||||
ReaderEvent.Navigation -> Unit
|
ReaderEvent.Navigation -> Unit
|
||||||
is ReaderEvent.BookReady -> {
|
is ReaderEvent.BookReady -> {
|
||||||
bookReady = true
|
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) {
|
private fun observeNativeReaderTap(event: MotionEvent) {
|
||||||
when (event.actionMasked) {
|
when (event.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
@@ -694,38 +704,46 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
val elapsed = event.eventTime - readerTouchDownAt
|
val elapsed = event.eventTime - readerTouchDownAt
|
||||||
val normalizedX = event.x / binding.readerWebView.width.coerceAtLeast(1)
|
|
||||||
val modalVisible = binding.readerLoadingOverlay.isVisible ||
|
val modalVisible = binding.readerLoadingOverlay.isVisible ||
|
||||||
binding.readerSettingsOverlay.isVisible ||
|
binding.readerSettingsOverlay.isVisible ||
|
||||||
binding.readerContentsOverlay.isVisible ||
|
binding.readerContentsOverlay.isVisible ||
|
||||||
binding.readerSelectionOverlay.isVisible ||
|
binding.readerSelectionOverlay.isVisible ||
|
||||||
binding.noteEditorOverlay.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 &&
|
!readerTouchMoved &&
|
||||||
elapsed in 0..NATIVE_TAP_MAX_DURATION_MS &&
|
elapsed in 0..NATIVE_TAP_MAX_DURATION_MS &&
|
||||||
normalizedX in READER_CENTER_ZONE_START..READER_CENTER_ZONE_END &&
|
|
||||||
bookReady &&
|
bookReady &&
|
||||||
!finishingReader &&
|
!finishingReader &&
|
||||||
currentSelection == null &&
|
currentSelection == null &&
|
||||||
!modalVisible
|
!modalVisible &&
|
||||||
|
!interactiveTarget
|
||||||
readerTouchActive = false
|
readerTouchActive = false
|
||||||
readerTouchMoved = false
|
readerTouchMoved = false
|
||||||
if (shouldToggle) {
|
if (!shouldHandle) return
|
||||||
lastNativeControlsToggleAt = SystemClock.uptimeMillis()
|
|
||||||
toggleControls()
|
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() {
|
private fun renderChromeVisibility() {
|
||||||
binding.pageHeader.isVisible = preferences.showTitle &&
|
binding.pageHeader.isVisible = preferences.showTitle &&
|
||||||
!binding.readerControlsOverlay.isVisible &&
|
!binding.readerControlsOverlay.isVisible &&
|
||||||
@@ -1523,17 +1541,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBack() {
|
private fun handleBack() {
|
||||||
when {
|
finishReader()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openExternalLink(url: String) {
|
private fun openExternalLink(url: String) {
|
||||||
@@ -1719,9 +1727,8 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
private const val PROGRESS_SAVE_DEBOUNCE_MS = 1_500L
|
private const val PROGRESS_SAVE_DEBOUNCE_MS = 1_500L
|
||||||
private const val SETTINGS_SAVE_DEBOUNCE_MS = 180L
|
private const val SETTINGS_SAVE_DEBOUNCE_MS = 180L
|
||||||
private const val NATIVE_TAP_MAX_DURATION_MS = 350L
|
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 = 1f / 3f
|
||||||
private const val READER_CENTER_ZONE_START = 0.28f
|
private const val READER_CENTER_ZONE_END = 2f / 3f
|
||||||
private const val READER_CENTER_ZONE_END = 0.72f
|
|
||||||
private val FATAL_ERROR_STAGES = setOf(
|
private val FATAL_ERROR_STAGES = setOf(
|
||||||
"book.load",
|
"book.load",
|
||||||
"api.loadBook",
|
"api.loadBook",
|
||||||
|
|||||||
@@ -629,13 +629,13 @@
|
|||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="60dp"
|
android:layout_width="44dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="44dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="@drawable/bg_reader_v2_control"
|
android:background="@drawable/bg_reader_v2_control"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:padding="17dp"
|
android:padding="6dp"
|
||||||
android:src="@drawable/ic_reader_v2_minus"
|
android:src="@drawable/ic_reader_v2_minus"
|
||||||
android:tag="reader_v2_control" />
|
android:tag="reader_v2_control" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
@@ -650,13 +650,13 @@
|
|||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="60dp"
|
android:layout_width="44dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="44dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="@drawable/bg_reader_v2_control"
|
android:background="@drawable/bg_reader_v2_control"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:padding="13dp"
|
android:padding="6dp"
|
||||||
android:src="@drawable/ic_reader_v2_plus"
|
android:src="@drawable/ic_reader_v2_plus"
|
||||||
android:tag="reader_v2_control" />
|
android:tag="reader_v2_control" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
@@ -832,13 +832,13 @@
|
|||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="60dp"
|
android:layout_width="44dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="44dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="@drawable/bg_reader_v2_control"
|
android:background="@drawable/bg_reader_v2_control"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:padding="17dp"
|
android:padding="6dp"
|
||||||
android:src="@drawable/ic_reader_v2_minus"
|
android:src="@drawable/ic_reader_v2_minus"
|
||||||
android:tag="reader_v2_control" />
|
android:tag="reader_v2_control" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
@@ -853,13 +853,13 @@
|
|||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="60dp"
|
android:layout_width="44dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="44dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="@drawable/bg_reader_v2_control"
|
android:background="@drawable/bg_reader_v2_control"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:padding="13dp"
|
android:padding="6dp"
|
||||||
android:src="@drawable/ic_reader_v2_plus"
|
android:src="@drawable/ic_reader_v2_plus"
|
||||||
android:tag="reader_v2_control" />
|
android:tag="reader_v2_control" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="reader_no_book_selected">Книга для чтения не выбрана</string>
|
||||||
<string name="app_name">Алетея</string>
|
<string name="app_name">Алетея</string>
|
||||||
|
|
||||||
<string name="tab_library">Мои книги</string>
|
<string name="tab_library">Мои книги</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user