Epub improvements (#160)
* Improved navigation and search result highlighting in the EPUB reader * refactor: implement JIT chunk restoration for robust navigation - Fixes search and bookmark navigation failing in same-chapter transitions. - Introduced JIT (Just-in-Time) HTML restoration in `epub_reader.js` to re-populate virtualized chunks before CFI resolution or search scrolling. - Synchronized search highlighting and user highlights within newly-restored chunk segments. - Improved search navigation accuracy by mapping occurrences to relative chunk indices. * Implemented a custom text selection engine for the paginated EPUB reader to support cross-page selection and improved handle interaction. * Implemented management of external file behavior and improved library filtering. * Added support for toolbar customization in the EPUB reader. * Added support for toolbar customization in the PDF reader. * fix: rendering during auto-scroll and navigation on long pages * Added a "Scroll to Top" feature to the auto-scroll controls in both EPUB and PDF readers.
This commit is contained in:
parent
db7e05ce63
commit
6a60aec0ef
17 changed files with 3045 additions and 2060 deletions
|
|
@ -240,6 +240,8 @@ data class ReaderScreenState(
|
|||
val openTabIds: List<String> = emptyList(),
|
||||
val openTabs: List<RecentFileItem> = emptyList(),
|
||||
val activeTabBookId: String? = null,
|
||||
val showExternalFileSavePromptFor: String? = null,
|
||||
val externalFileBehavior: String = "ASK",
|
||||
)
|
||||
|
||||
open class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
|
@ -279,6 +281,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
@Suppress("unused")
|
||||
val navigationEvent = _navigationEvent.receiveAsFlow()
|
||||
private var pendingSwitchDeferred: CompletableDeferred<Boolean>? = null
|
||||
private var externalOpenedBookId: String? = null
|
||||
|
||||
data class PageModificationResult(
|
||||
val layout: List<VirtualPage>,
|
||||
|
|
@ -348,6 +351,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
} catch(_: Exception) { emptyList() }
|
||||
} ?: emptyList(),
|
||||
activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null),
|
||||
externalFileBehavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK"
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -370,7 +374,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
val filters = internalState.libraryFilters
|
||||
val libraryFiltered = baseVisibleFiles.filter { item ->
|
||||
val matchType = if (filters.fileTypes.isNotEmpty()) item.type in filters.fileTypes else true
|
||||
val matchFolder = if (filters.sourceFolders.isNotEmpty()) item.sourceFolderUri in filters.sourceFolders else true
|
||||
val matchFolder = if (filters.sourceFolders.isNotEmpty()) {
|
||||
val matchesInApp = filters.sourceFolders.contains("IN_APP_STORAGE") && item.sourceFolderUri == null && item.uriString?.startsWith("opds-pse") != true
|
||||
val matchesSynced = item.sourceFolderUri in filters.sourceFolders
|
||||
matchesInApp || matchesSynced
|
||||
} else true
|
||||
val progress = item.progressPercentage ?: 0f
|
||||
val matchStatus = when (filters.readStatus) {
|
||||
ReadStatusFilter.ALL -> true
|
||||
|
|
@ -963,7 +971,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
|
||||
withContext(Dispatchers.Main) {
|
||||
onDeleted()
|
||||
showBanner(appContext.getString(R.string.banner_text_view_deleted))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1487,6 +1494,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
}
|
||||
}
|
||||
|
||||
val closingBookId = _internalState.value.selectedBookId
|
||||
val uriString = _internalState.value.selectedPdfUri?.toString()
|
||||
?: _internalState.value.selectedEpubUri?.toString()
|
||||
|
||||
|
|
@ -1504,6 +1512,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
)
|
||||
}
|
||||
|
||||
if (closingBookId != null && closingBookId == externalOpenedBookId) {
|
||||
val behavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK"
|
||||
if (behavior == "ASK") {
|
||||
_internalState.update { it.copy(showExternalFileSavePromptFor = closingBookId) }
|
||||
} else if (behavior == "DELETE") {
|
||||
deleteBookPermanently(closingBookId)
|
||||
}
|
||||
externalOpenedBookId = null
|
||||
}
|
||||
|
||||
if (uriString != null) {
|
||||
viewModelScope.launch {
|
||||
val freshBook = recentFilesRepository.getFileByUri(uriString)
|
||||
|
|
@ -2819,7 +2837,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
}
|
||||
}
|
||||
|
||||
fun onFileSelected(uri: Uri, isFromRecent: Boolean = false) {
|
||||
fun onFileSelected(uri: Uri, isFromRecent: Boolean = false, isExternalIntent: Boolean = false) {
|
||||
if (isFromRecent) {
|
||||
Timber.i("Opening recent file: $uri")
|
||||
viewModelScope.launch {
|
||||
|
|
@ -2832,11 +2850,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
}
|
||||
} else {
|
||||
Timber.i("Importing new file: $uri")
|
||||
importExternalFile(uri)
|
||||
importExternalFile(uri, isExternalIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importExternalFile(externalUri: Uri) {
|
||||
private fun importExternalFile(externalUri: Uri, isExternalIntent: Boolean = false) {
|
||||
_internalState.update {
|
||||
it.copy(isLoading = true, errorMessage = null, contextualActionItems = emptySet())
|
||||
}
|
||||
|
|
@ -2846,6 +2864,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
|
||||
if (importResult != null) {
|
||||
val (internalUri, bookId, type) = importResult
|
||||
if (isExternalIntent) {
|
||||
externalOpenedBookId = bookId
|
||||
}
|
||||
val displayName = getFileNameFromUri(externalUri, appContext) ?: "Unknown File"
|
||||
openBook(
|
||||
internalUri, bookId = bookId, type = type, originalDisplayName = displayName
|
||||
|
|
@ -3734,12 +3755,25 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
}
|
||||
|
||||
fun selectAllRecentFiles() {
|
||||
val recentFilesForHome = uiState.value.recentFiles.filter { it.isRecent }
|
||||
_internalState.update { it.copy(contextualActionItems = recentFilesForHome.toSet()) }
|
||||
val currentVisible = uiState.value.recentFiles.filter { it.isRecent }.toSet()
|
||||
_internalState.update { state ->
|
||||
if (state.contextualActionItems.containsAll(currentVisible) && currentVisible.isNotEmpty()) {
|
||||
state.copy(contextualActionItems = emptySet())
|
||||
} else {
|
||||
state.copy(contextualActionItems = currentVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAllLibraryFiles() {
|
||||
_internalState.update { it.copy(contextualActionItems = uiState.value.recentFiles.toSet()) }
|
||||
val currentVisible = uiState.value.allRecentFiles.toSet()
|
||||
_internalState.update { state ->
|
||||
if (state.contextualActionItems.containsAll(currentVisible) && currentVisible.isNotEmpty()) {
|
||||
state.copy(contextualActionItems = emptySet())
|
||||
} else {
|
||||
state.copy(contextualActionItems = currentVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearContextualAction() {
|
||||
|
|
@ -3753,6 +3787,21 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
_internalState.update { it.copy(showCreateShelfDialog = true) }
|
||||
}
|
||||
|
||||
fun handleExternalFilePrompt(bookId: String, keep: Boolean, dontAskAgain: Boolean) {
|
||||
if (dontAskAgain) {
|
||||
val newBehavior = if (keep) "KEEP" else "DELETE"
|
||||
setExternalFileBehavior(newBehavior)
|
||||
}
|
||||
if (!keep) {
|
||||
deleteBookPermanently(bookId)
|
||||
}
|
||||
_internalState.update { it.copy(showExternalFileSavePromptFor = null) }
|
||||
}
|
||||
fun setExternalFileBehavior(behavior: String) {
|
||||
prefs.edit { putString(KEY_EXTERNAL_FILE_BEHAVIOR, behavior) }
|
||||
_internalState.update { it.copy(externalFileBehavior = behavior) }
|
||||
}
|
||||
|
||||
fun dismissCreateShelfDialog() {
|
||||
_internalState.update { it.copy(showCreateShelfDialog = false) }
|
||||
}
|
||||
|
|
@ -4361,5 +4410,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
private const val KEY_TABS_ENABLED = "tabs_enabled"
|
||||
private const val KEY_OPEN_TAB_IDS = "open_tab_ids"
|
||||
private const val KEY_ACTIVE_TAB = "active_tab_book_id"
|
||||
private const val KEY_EXTERNAL_FILE_BEHAVIOR = "external_file_behavior"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue