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
|
|
@ -774,15 +774,16 @@
|
|||
document.addEventListener("DOMContentLoaded", initializeReaderContent);
|
||||
}
|
||||
|
||||
window.CURRENT_SEARCH_QUERY = "";
|
||||
|
||||
window.clearSearchHighlights = function () {
|
||||
window.CURRENT_SEARCH_QUERY = "";
|
||||
document.querySelectorAll("mark.search-highlight").forEach(function (el) {
|
||||
var parent = el.parentNode;
|
||||
|
||||
if (parent) {
|
||||
while (el.firstChild) {
|
||||
parent.insertBefore(el.firstChild, el);
|
||||
}
|
||||
|
||||
parent.removeChild(el);
|
||||
parent.normalize();
|
||||
}
|
||||
|
|
@ -792,10 +793,12 @@
|
|||
|
||||
window.highlightAllOccurrences = function (query) {
|
||||
window.clearSearchHighlights();
|
||||
window.CURRENT_SEARCH_QUERY = query;
|
||||
|
||||
if (!query || query.length < 2) return "JS: Query too short for highlighting.";
|
||||
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
|
||||
var nodesToModify = [];
|
||||
var nodesToModify =[];
|
||||
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeValue.toLowerCase().includes(query.toLowerCase())) {
|
||||
|
|
@ -811,11 +814,9 @@
|
|||
tempDiv.innerHTML = textNode.nodeValue.replace(regex, '<mark class="search-highlight">$1</mark>');
|
||||
|
||||
var parent = textNode.parentNode;
|
||||
|
||||
while (tempDiv.firstChild) {
|
||||
parent.insertBefore(tempDiv.firstChild, textNode);
|
||||
}
|
||||
|
||||
parent.removeChild(textNode);
|
||||
}
|
||||
});
|
||||
|
|
@ -823,17 +824,37 @@
|
|||
return "JS: Highlighted " + document.querySelectorAll("mark.search-highlight").length + " occurrences.";
|
||||
};
|
||||
|
||||
window.scrollToOccurrence = function (index) {
|
||||
var highlights = document.querySelectorAll("mark.search-highlight");
|
||||
window.scrollToChunkOccurrence = function (chunkIndex, relativeIndex) {
|
||||
console.log("NavDiag: scrollToChunkOccurrence chunk=" + chunkIndex + ", relativeIdx=" + relativeIndex);
|
||||
var chunkDiv = document.querySelector(`.chunk-container[data-chunk-index='${chunkIndex}']`);
|
||||
|
||||
if (highlights && index >= 0 && index < highlights.length) {
|
||||
var element = highlights[index];
|
||||
if (chunkDiv) {
|
||||
let wasEmpty = false;
|
||||
|
||||
element.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
|
||||
return "JS: Scrolled to occurrence " + index;
|
||||
if (chunkDiv.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) {
|
||||
console.log("NavDiag: Chunk was empty, restoring content before scrolling.");
|
||||
chunkDiv.innerHTML = window.virtualization.chunksData[chunkIndex];
|
||||
chunkDiv.style.height = "";
|
||||
wasEmpty = true;
|
||||
}
|
||||
|
||||
return "JS: Occurrence " + index + " not found.";
|
||||
var highlights = chunkDiv.querySelectorAll("mark.search-highlight");
|
||||
|
||||
if ((wasEmpty || highlights.length === 0) && window.CURRENT_SEARCH_QUERY) {
|
||||
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
|
||||
highlights = chunkDiv.querySelectorAll("mark.search-highlight");
|
||||
}
|
||||
|
||||
if (highlights && highlights.length > 0) {
|
||||
var targetIdx = (relativeIndex >= 0 && relativeIndex < highlights.length) ? relativeIndex : 0;
|
||||
highlights[targetIdx].scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
|
||||
return "JS: Scrolled to relative occurrence " + targetIdx + " in chunk " + chunkIndex;
|
||||
} else {
|
||||
chunkDiv.scrollIntoView({ behavior: "auto", block: "center" });
|
||||
return "JS: No highlights in chunk, scrolled to chunk center.";
|
||||
}
|
||||
}
|
||||
return "JS: Chunk " + chunkIndex + " not found.";
|
||||
};
|
||||
|
||||
window.removeHighlight = function () {
|
||||
|
|
@ -1429,6 +1450,15 @@
|
|||
|
||||
let chunkElement = currentNode.querySelector(`.chunk-container[data-chunk-index="${chunkIndex}"]`);
|
||||
if (chunkElement) {
|
||||
if (chunkElement.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) {
|
||||
console.log("CFI_DIAGNOSIS: Chunk " + chunkIndex + " was empty, restoring content for CFI resolution.");
|
||||
chunkElement.innerHTML = window.virtualization.chunksData[chunkIndex];
|
||||
chunkElement.style.height = "";
|
||||
if (window.CURRENT_HIGHLIGHTS) {
|
||||
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||
}
|
||||
}
|
||||
|
||||
let elementsInChunk = Array.from(chunkElement.childNodes).filter(n => n.nodeType === Node.ELEMENT_NODE);
|
||||
if (indexInChunk >= 0 && indexInChunk < elementsInChunk.length) {
|
||||
currentNode = elementsInChunk[indexInChunk];
|
||||
|
|
@ -1649,7 +1679,7 @@
|
|||
cleanCfi = cfi.substring(cfi.indexOf('@') + 1);
|
||||
}
|
||||
|
||||
console.log("PosSaveDiag: JS scrollToCfi called with cleanCfi=" + cleanCfi);
|
||||
console.log("NavDiag: JS scrollToCfi called with cleanCfi=" + cleanCfi);
|
||||
|
||||
if (!cleanCfi || !cleanCfi.startsWith('/')) {
|
||||
if (window.CfiBridge && window.CfiBridge.onScrollFinished) {
|
||||
|
|
@ -1719,6 +1749,7 @@
|
|||
}
|
||||
|
||||
if (Math.abs(window.scrollY - targetScrollY) > 1) {
|
||||
console.log("NavDiag: Scrolling to targetY=" + targetScrollY);
|
||||
window.scrollTo({ top: targetScrollY, behavior: 'auto' });
|
||||
}
|
||||
|
||||
|
|
@ -1947,6 +1978,9 @@
|
|||
if (window.CURRENT_HIGHLIGHTS) {
|
||||
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||
}
|
||||
if (window.CURRENT_SEARCH_QUERY) {
|
||||
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (div.innerHTML !== "") {
|
||||
|
|
@ -2005,6 +2039,10 @@
|
|||
if (window.CURRENT_HIGHLIGHTS) {
|
||||
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||
}
|
||||
|
||||
if (window.CURRENT_SEARCH_QUERY) {
|
||||
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.checkImagesForDiagnosis) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import androidx.compose.material3.AlertDialog
|
|||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
|
|
@ -83,6 +84,7 @@ import androidx.compose.material3.ModalDrawerSheet
|
|||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
|
|
@ -163,6 +165,7 @@ fun HomeScreen(
|
|||
var showAboutDialog by remember { mutableStateOf(false) }
|
||||
var showInfoDialog by remember { mutableStateOf(false) }
|
||||
var itemForInfoDialog by remember { mutableStateOf<RecentFileItem?>(null) }
|
||||
var showBehaviorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var showClearBookCacheDialog by remember { mutableStateOf(false) }
|
||||
var showClearReflowCacheDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -308,7 +311,8 @@ fun HomeScreen(
|
|||
onFolderSyncToggle = viewModel::setFolderSyncEnabled,
|
||||
onClearReflowCache = { showClearReflowCacheDialog = true },
|
||||
onRecentFilesLimitChange = viewModel::setRecentFilesLimit,
|
||||
onTabsToggle = viewModel::setTabsEnabled
|
||||
onTabsToggle = viewModel::setTabsEnabled,
|
||||
onExternalFileBehaviorClick = { showBehaviorDialog = true }
|
||||
)
|
||||
} else {
|
||||
ContextualTopAppBar(
|
||||
|
|
@ -443,6 +447,20 @@ fun HomeScreen(
|
|||
onDismiss = { showClearReflowCacheDialog = false }
|
||||
)
|
||||
}
|
||||
if (uiState.showExternalFileSavePromptFor != null) {
|
||||
ExternalFileSaveDialog(
|
||||
onConfirm = { keep, dontAskAgain ->
|
||||
viewModel.handleExternalFilePrompt(uiState.showExternalFileSavePromptFor!!, keep, dontAskAgain)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showBehaviorDialog) {
|
||||
ExternalFileBehaviorDialog(
|
||||
currentBehavior = uiState.externalFileBehavior,
|
||||
onDismiss = { showBehaviorDialog = false },
|
||||
onSelect = { viewModel.setExternalFileBehavior(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showAboutDialog) {
|
||||
|
|
@ -758,7 +776,8 @@ fun DefaultTopAppBar(
|
|||
onShowDeviceManagement: () -> Unit,
|
||||
onFolderSyncToggle: (Boolean) -> Unit,
|
||||
onRecentFilesLimitChange: (Int) -> Unit,
|
||||
onTabsToggle: (Boolean) -> Unit
|
||||
onTabsToggle: (Boolean) -> Unit,
|
||||
onExternalFileBehaviorClick: () -> Unit
|
||||
) {
|
||||
var showOptionsMenu by remember { mutableStateOf(false) }
|
||||
var showLimitMenu by remember { mutableStateOf(false) }
|
||||
|
|
@ -822,6 +841,11 @@ fun DefaultTopAppBar(
|
|||
}
|
||||
})
|
||||
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.options_external_file_behavior)) }, onClick = {
|
||||
onExternalFileBehaviorClick()
|
||||
showOptionsMenu = false
|
||||
})
|
||||
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_book_cache)) }, onClick = {
|
||||
onClearCache()
|
||||
|
|
@ -1285,3 +1309,77 @@ fun DangerousFolderActionDialog(
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExternalFileSaveDialog(
|
||||
onConfirm: (keep: Boolean, dontAskAgain: Boolean) -> Unit
|
||||
) {
|
||||
var dontAsk by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { },
|
||||
properties = androidx.compose.ui.window.DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
title = { Text(stringResource(R.string.external_file_prompt_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringResource(R.string.external_file_prompt_desc))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { dontAsk = !dontAsk }
|
||||
) {
|
||||
Checkbox(checked = dontAsk, onCheckedChange = { dontAsk = it })
|
||||
Text(stringResource(R.string.external_file_dont_ask))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onConfirm(true, dontAsk) }) {
|
||||
Text(stringResource(R.string.external_file_keep))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { onConfirm(false, dontAsk) },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
|
||||
) {
|
||||
Text(stringResource(R.string.external_file_delete))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExternalFileBehaviorDialog(
|
||||
currentBehavior: String,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (String) -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.options_external_file_behavior)) },
|
||||
text = {
|
||||
Column {
|
||||
val options = listOf("ASK" to R.string.external_file_behavior_ask, "KEEP" to R.string.external_file_behavior_keep, "DELETE" to R.string.external_file_behavior_delete)
|
||||
options.forEach { (value, labelRes) ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect(value); onDismiss() }
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
RadioButton(selected = currentBehavior == value, onClick = null)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(stringResource(labelRes))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1878,12 +1878,19 @@ fun LibraryFilterSheet(
|
|||
}
|
||||
}
|
||||
|
||||
if (syncedFolders.isNotEmpty()) {
|
||||
Text(stringResource(R.string.filter_source_folder), style = MaterialTheme.typography.titleMedium)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = "IN_APP_STORAGE" in currentFilters.sourceFolders,
|
||||
onClick = {
|
||||
val newSet = if ("IN_APP_STORAGE" in currentFilters.sourceFolders) currentFilters.sourceFolders - "IN_APP_STORAGE" else currentFilters.sourceFolders + "IN_APP_STORAGE"
|
||||
currentFilters = currentFilters.copy(sourceFolders = newSet)
|
||||
},
|
||||
label = { Text(stringResource(R.string.filter_in_app_storage)) }
|
||||
)
|
||||
syncedFolders.forEach { folder ->
|
||||
FilterChip(
|
||||
selected = folder.uriString in currentFilters.sourceFolders,
|
||||
|
|
@ -1895,7 +1902,6 @@ fun LibraryFilterSheet(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.filter_read_status), style = MaterialTheme.typography.titleMedium)
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class MainActivity : ComponentActivity() {
|
|||
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||
Timber.d("Received VIEW intent with URI: ${intent.data}")
|
||||
val uri = intent.data!!
|
||||
viewModel.onFileSelected(uri)
|
||||
viewModel.onFileSelected(uri, isFromRecent = false, isExternalIntent = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -577,6 +577,11 @@ fun ChapterWebView(
|
|||
)
|
||||
}
|
||||
|
||||
message.startsWith("NavDiag:") -> {
|
||||
Timber.tag("NavDiag")
|
||||
.d("JS -> ${message.substringAfter("NavDiag: ")}")
|
||||
}
|
||||
|
||||
message.startsWith("AutoScrollDiagnosis") -> {
|
||||
Timber.d(
|
||||
"JS -> ${message.substringAfter("AutoScrollDiagnosis: ")}"
|
||||
|
|
@ -726,13 +731,13 @@ fun ChapterWebView(
|
|||
|
||||
if (!initialCfi.isNullOrBlank()) {
|
||||
val cfiJsCommand = "javascript:window.scrollToCfi('$initialCfi');"
|
||||
Timber.tag("POS_DIAG").d("WebView onPageFinished: Triggering scroll to initialCfi: $initialCfi")
|
||||
Timber.tag("NavDiag").d("WebView onPageFinished: Triggering scroll to initialCfi: $initialCfi")
|
||||
view?.evaluateJavascript(cfiJsCommand) {
|
||||
onChapterInitiallyScrolled()
|
||||
scrollActionTaken = true
|
||||
}
|
||||
} else if (!initialFragmentId.isNullOrBlank()) {
|
||||
Timber.d("WebView onPageFinished: Scrolling to Element ID: $initialFragmentId")
|
||||
Timber.tag("NavDiag").d("WebView onPageFinished: Scrolling to Element ID: $initialFragmentId")
|
||||
view?.evaluateJavascript(
|
||||
"javascript:var el = document.getElementById('$initialFragmentId'); if(el) { el.scrollIntoView(); } else { console.log('Element not found: $initialFragmentId'); }",
|
||||
null
|
||||
|
|
@ -744,9 +749,7 @@ fun ChapterWebView(
|
|||
ChapterScrollPosition.END -> "javascript:window.scrollToChapterEnd();"
|
||||
else -> "javascript:window.scrollToChapterStart();"
|
||||
}
|
||||
Timber.d(
|
||||
"WebView onPageFinished: Executing initial scroll to target: $initialScrollTarget"
|
||||
)
|
||||
Timber.tag("NavDiag").d("WebView onPageFinished: Executing initial scroll to target: $initialScrollTarget")
|
||||
view?.evaluateJavascript(scrollJsCommand) {
|
||||
onChapterInitiallyScrolled()
|
||||
scrollActionTaken = true
|
||||
|
|
@ -754,17 +757,13 @@ fun ChapterWebView(
|
|||
} else if (initialPageScrollY != null && initialPageScrollY > 0) {
|
||||
val scrollJsCommand =
|
||||
"javascript:window.scrollToSpecificY($initialPageScrollY);"
|
||||
Timber.d(
|
||||
"WebView onPageFinished: Executing initial scroll to Y: $initialPageScrollY"
|
||||
)
|
||||
Timber.tag("NavDiag").d("WebView onPageFinished: Executing initial scroll to Y: $initialPageScrollY")
|
||||
view?.evaluateJavascript(scrollJsCommand) {
|
||||
onChapterInitiallyScrolled()
|
||||
scrollActionTaken = true
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"WebView onPageFinished: No specific scroll, defaulting to start."
|
||||
)
|
||||
Timber.tag("NavDiag").d("WebView onPageFinished: No specific scroll, defaulting to start.")
|
||||
view?.evaluateJavascript("javascript:window.scrollToChapterStart();") {
|
||||
onChapterInitiallyScrolled()
|
||||
scrollActionTaken = true
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
|
|
@ -69,6 +74,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ChevronLeft
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
|
|
@ -131,6 +137,26 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class ReaderTool(val title: String, val category: String) {
|
||||
DICTIONARY("External Apps", "Top Bar"),
|
||||
THEME("Theme Settings", "Top Bar"),
|
||||
SLIDER("Navigation Slider", "Bottom Bar"),
|
||||
TOC("Sidebar", "Bottom Bar"),
|
||||
FORMAT("Text Formatting", "Bottom Bar"),
|
||||
SEARCH("Search", "Bottom Bar"),
|
||||
AI_FEATURES("AI Features", "Bottom Bar"),
|
||||
TTS_CONTROLS("TTS Controls", "Bottom Bar"),
|
||||
READING_MODE("Reading Mode", "Overflow Menu"),
|
||||
BOOKMARK("Bookmark", "Overflow Menu"),
|
||||
TAP_TO_TURN("Tap to Turn Pages", "Overflow Menu"),
|
||||
VOLUME_SCROLL("Volume Button Scrolling", "Overflow Menu"),
|
||||
PAGE_TURN_ANIM("Realistic Page Turns", "Overflow Menu"),
|
||||
KEEP_SCREEN_ON("Keep Screen On", "Overflow Menu"),
|
||||
VISUAL_OPTIONS("Visual Options", "Overflow Menu"),
|
||||
AUTO_SCROLL("Auto Scroll", "Overflow Menu"),
|
||||
TTS_SETTINGS("TTS Voice Settings", "Overflow Menu")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpubReaderTopBar(
|
||||
isVisible: Boolean,
|
||||
|
|
@ -158,6 +184,8 @@ fun EpubReaderTopBar(
|
|||
onOpenThemeSettings: () -> Unit,
|
||||
onOpenVisualOptions: () -> Unit,
|
||||
searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
|
||||
hiddenTools: Set<String>,
|
||||
onCustomizeTools: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onToggleReflow: (() -> Unit)? = null,
|
||||
onDeleteReflow: (() -> Unit)? = null,
|
||||
|
|
@ -204,6 +232,7 @@ fun EpubReaderTopBar(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (!hiddenTools.contains(ReaderTool.DICTIONARY.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
|
|
@ -214,6 +243,8 @@ fun EpubReaderTopBar(
|
|||
contentDescription = stringResource(R.string.content_desc_dictionary_settings)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.THEME.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_theme),
|
||||
description = stringResource(R.string.tooltip_theme_desc),
|
||||
|
|
@ -221,6 +252,7 @@ fun EpubReaderTopBar(
|
|||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc))
|
||||
}
|
||||
}
|
||||
Box {
|
||||
var showMoreMenu by remember { mutableStateOf(false) }
|
||||
TooltipIconButton(
|
||||
|
|
@ -235,6 +267,18 @@ fun EpubReaderTopBar(
|
|||
expanded = showMoreMenu,
|
||||
onDismissRequest = { showMoreMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Customize Toolbar") },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
onCustomizeTools()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
if (onToggleReflow != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_view_original_pdf)) },
|
||||
|
|
@ -274,6 +318,7 @@ fun EpubReaderTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(ReaderTool.READING_MODE.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_reading_mode_vertical)) },
|
||||
enabled = !isTtsActive,
|
||||
|
|
@ -281,8 +326,12 @@ fun EpubReaderTopBar(
|
|||
showMoreMenu = false
|
||||
onChangeRenderMode(RenderMode.VERTICAL_SCROLL)
|
||||
},
|
||||
trailingIcon = { if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
|
||||
trailingIcon = {
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_selected)
|
||||
)
|
||||
})
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_reading_mode_paginated)) },
|
||||
enabled = !isTtsActive,
|
||||
|
|
@ -290,17 +339,28 @@ fun EpubReaderTopBar(
|
|||
showMoreMenu = false
|
||||
onChangeRenderMode(RenderMode.PAGINATED)
|
||||
},
|
||||
trailingIcon = { if (currentRenderMode == RenderMode.PAGINATED) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
|
||||
trailingIcon = {
|
||||
if (currentRenderMode == RenderMode.PAGINATED) Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_selected)
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isBookmarked) stringResource(R.string.menu_remove_bookmark) else stringResource(R.string.menu_bookmark_this_page)) },
|
||||
onClick = {
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.BOOKMARK.name)) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (isBookmarked) stringResource(R.string.menu_remove_bookmark) else stringResource(
|
||||
R.string.menu_bookmark_this_page
|
||||
)
|
||||
)
|
||||
}, onClick = {
|
||||
showMoreMenu = false
|
||||
onToggleBookmark()
|
||||
}
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.TAP_TO_TURN.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_tap_to_turn_pages)) },
|
||||
enabled = currentRenderMode == RenderMode.PAGINATED,
|
||||
|
|
@ -308,13 +368,21 @@ fun EpubReaderTopBar(
|
|||
onToggleTapToNavigate(!tapToNavigateEnabled)
|
||||
showMoreMenu = false
|
||||
},
|
||||
trailingIcon = { if (tapToNavigateEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
|
||||
trailingIcon = {
|
||||
if (tapToNavigateEnabled) Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_enabled)
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.VOLUME_SCROLL.name)) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) stringResource(R.string.menu_volume_button_scrolling)
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) stringResource(
|
||||
R.string.menu_volume_button_scrolling
|
||||
)
|
||||
else stringResource(R.string.menu_volume_button_page_turn)
|
||||
)
|
||||
},
|
||||
|
|
@ -323,10 +391,15 @@ fun EpubReaderTopBar(
|
|||
onToggleVolumeScroll(!volumeScrollEnabled)
|
||||
showMoreMenu = false
|
||||
},
|
||||
trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
|
||||
trailingIcon = {
|
||||
if (volumeScrollEnabled) Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_enabled)
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.PAGE_TURN_ANIM.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_realistic_page_turns)) },
|
||||
enabled = currentRenderMode == RenderMode.PAGINATED,
|
||||
|
|
@ -334,20 +407,30 @@ fun EpubReaderTopBar(
|
|||
onTogglePageTurnAnimation(!isPageTurnAnimationEnabled)
|
||||
showMoreMenu = false
|
||||
},
|
||||
trailingIcon = { if (isPageTurnAnimationEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
|
||||
trailingIcon = {
|
||||
if (isPageTurnAnimationEnabled) Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_enabled)
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.KEEP_SCREEN_ON.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_keep_screen_on)) },
|
||||
onClick = {
|
||||
onToggleKeepScreenOn(!isKeepScreenOn)
|
||||
showMoreMenu = false
|
||||
},
|
||||
trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
|
||||
trailingIcon = {
|
||||
if (isKeepScreenOn) Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_enabled)
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.VISUAL_OPTIONS.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_visual_options)) },
|
||||
onClick = {
|
||||
|
|
@ -355,22 +438,25 @@ fun EpubReaderTopBar(
|
|||
onOpenVisualOptions()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Visibility, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.Visibility,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.AUTO_SCROLL.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_auto_scroll)) },
|
||||
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
onStartAutoScroll()
|
||||
}
|
||||
)
|
||||
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.TTS_SETTINGS.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_tts_voice_settings)) },
|
||||
onClick = {
|
||||
|
|
@ -378,9 +464,12 @@ fun EpubReaderTopBar(
|
|||
onOpenDeviceVoiceSettings()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.GraphicEq,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
DropdownMenuItem(
|
||||
|
|
@ -390,7 +479,11 @@ fun EpubReaderTopBar(
|
|||
onOpenTtsSettings()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(painter = painterResource(id = R.drawable.text_to_speech), contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.text_to_speech),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -400,6 +493,7 @@ fun EpubReaderTopBar(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
|
|
@ -418,6 +512,7 @@ fun EpubReaderBottomBar(
|
|||
onRecap: () -> Unit,
|
||||
onToggleTts: () -> Unit,
|
||||
onPlayPauseTts: () -> Unit,
|
||||
hiddenTools: Set<String>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
|
|
@ -439,58 +534,81 @@ fun EpubReaderBottomBar(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
if (!hiddenTools.contains(ReaderTool.SLIDER.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = onOpenSlider,
|
||||
enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL
|
||||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.slider),
|
||||
contentDescription = stringResource(R.string.content_desc_navigate_slider)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.TOC.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
onClick = onOpenDrawer
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_chapters_menu))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(R.string.content_desc_chapters_menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.FORMAT.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_format),
|
||||
description = stringResource(R.string.tooltip_format_desc),
|
||||
onClick = onToggleFormat
|
||||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.format_size), contentDescription = stringResource(R.string.content_desc_text_formatting))
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.format_size),
|
||||
contentDescription = stringResource(R.string.content_desc_text_formatting)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.SEARCH.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
onClick = onToggleSearch
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Search, contentDescription = stringResource(R.string.tooltip_search))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.tooltip_search)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
if (BuildConfig.FLAVOR != "oss") {
|
||||
if (!hiddenTools.contains(ReaderTool.AI_FEATURES.name)) {
|
||||
@Suppress(
|
||||
"KotlinConstantConditions",
|
||||
"SimplifyBooleanWithConstants"
|
||||
) if (BuildConfig.FLAVOR != "oss") {
|
||||
Box {
|
||||
var showAiFeaturesMenu by remember { mutableStateOf(false) }
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_ai),
|
||||
description = stringResource(R.string.tooltip_ai_desc),
|
||||
onClick = { showAiFeaturesMenu = true }
|
||||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.ai), contentDescription = "AI Features")
|
||||
onClick = { showAiFeaturesMenu = true }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ai),
|
||||
contentDescription = "AI Features"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showAiFeaturesMenu,
|
||||
onDismissRequest = { showAiFeaturesMenu = false }
|
||||
) {
|
||||
onDismissRequest = { showAiFeaturesMenu = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_chapter_summarization)) },
|
||||
onClick = {
|
||||
showAiFeaturesMenu = false
|
||||
onSummarize()
|
||||
}
|
||||
)
|
||||
})
|
||||
if (BuildConfig.DEBUG && isProUser) {
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
|
|
@ -498,46 +616,46 @@ fun EpubReaderBottomBar(
|
|||
onClick = {
|
||||
showAiFeaturesMenu = false
|
||||
onRecap()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(ReaderTool.TTS_CONTROLS.name)) {
|
||||
Box {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
TooltipIconButton(
|
||||
text = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start_desc),
|
||||
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop)
|
||||
else stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc)
|
||||
else stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = onToggleTts
|
||||
) {
|
||||
Icon(
|
||||
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech),
|
||||
contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts)
|
||||
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(
|
||||
id = R.drawable.text_to_speech
|
||||
),
|
||||
contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(
|
||||
R.string.content_desc_start_tts
|
||||
)
|
||||
)
|
||||
}
|
||||
if (isTtsSessionActive) {
|
||||
TooltipIconButton(
|
||||
text = if (ttsState.isPlaying)
|
||||
stringResource(R.string.tooltip_tts_pause)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_resume),
|
||||
description = if (ttsState.isPlaying)
|
||||
stringResource(R.string.tooltip_tts_pause_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_resume_desc),
|
||||
text = if (ttsState.isPlaying) stringResource(R.string.tooltip_tts_pause)
|
||||
else stringResource(R.string.tooltip_tts_resume),
|
||||
description = if (ttsState.isPlaying) stringResource(R.string.tooltip_tts_pause_desc)
|
||||
else stringResource(R.string.tooltip_tts_resume_desc),
|
||||
onClick = onPlayPauseTts,
|
||||
enabled = !ttsState.isLoading
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
|
||||
contentDescription = if (ttsState.isPlaying) stringResource(R.string.content_desc_pause_tts) else stringResource(R.string.content_desc_resume_tts)
|
||||
contentDescription = if (ttsState.isPlaying) stringResource(
|
||||
R.string.content_desc_pause_tts
|
||||
) else stringResource(R.string.content_desc_resume_tts)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -546,6 +664,7 @@ fun EpubReaderBottomBar(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
|
|
@ -885,6 +1004,7 @@ fun AutoScrollControls(
|
|||
onLocalModeToggle: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isTempPaused: Boolean = false,
|
||||
onScrollToTop: (() -> Unit)? = null
|
||||
) {
|
||||
val backgroundAlpha = 0.6f
|
||||
|
||||
|
|
@ -1018,6 +1138,19 @@ fun AutoScrollControls(
|
|||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (onScrollToTop != null) {
|
||||
IconButton(
|
||||
onClick = onScrollToTop,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowUpward,
|
||||
contentDescription = "Scroll to Top",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = onMusicianModeToggle,
|
||||
modifier = Modifier.size(32.dp)
|
||||
|
|
@ -1217,3 +1350,75 @@ fun AutoScrollControls(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomizeToolsSheet(
|
||||
hiddenTools: Set<String>,
|
||||
onUpdate: (Set<String>) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
contentWindowInsets = { WindowInsets.navigationBars }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) {
|
||||
Text(
|
||||
text = "Customize Toolbar",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Select the tools you want to keep visible. Unchecking a tool hides it from the UI to give you a distraction-free reading space.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
ReaderTool.entries.groupBy { it.category }.forEach { (category, tools) ->
|
||||
item {
|
||||
Text(
|
||||
text = category,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
items(tools) { tool ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable {
|
||||
val newSet = hiddenTools.toMutableSet()
|
||||
if (newSet.contains(tool.name)) newSet.remove(tool.name)
|
||||
else newSet.add(tool.name)
|
||||
onUpdate(newSet)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = tool.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Switch(
|
||||
checked = !hiddenTools.contains(tool.name),
|
||||
onCheckedChange = { isVisible ->
|
||||
val newSet = hiddenTools.toMutableSet()
|
||||
if (isVisible) newSet.remove(tool.name) else newSet.add(tool.name)
|
||||
onUpdate(newSet)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -214,6 +214,17 @@ private const val AUTO_SCROLL_LOCAL_MIN_PREFIX = "auto_scroll_local_min_"
|
|||
private const val AUTO_SCROLL_LOCAL_MAX_PREFIX = "auto_scroll_local_max_"
|
||||
private const val MUSICIAN_MODE_KEY = "musician_mode_enabled"
|
||||
private const val KEEP_SCREEN_ON_KEY = "keep_screen_on_enabled"
|
||||
private const val HIDDEN_TOOLS_KEY = "hidden_reader_tools"
|
||||
|
||||
private fun saveHiddenTools(context: Context, hiddenTools: Set<String>) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit { putStringSet(HIDDEN_TOOLS_KEY, hiddenTools) }
|
||||
}
|
||||
|
||||
private fun loadHiddenTools(context: Context): Set<String> {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
return prefs.getStringSet(HIDDEN_TOOLS_KEY, emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
|
|
@ -655,6 +666,9 @@ fun EpubReaderHost(
|
|||
mutableStateOf(loadExternalSearchPackage(context))
|
||||
}
|
||||
|
||||
var hiddenTools by remember { mutableStateOf(loadHiddenTools(context)) }
|
||||
var showCustomizeToolsSheet by remember { mutableStateOf(false) }
|
||||
|
||||
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
|
||||
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -1603,6 +1617,7 @@ fun EpubReaderHost(
|
|||
}
|
||||
|
||||
fun navigateToSearchResult(index: Int) {
|
||||
Timber.tag("NavDiag").d("navigateToSearchResult index: $index")
|
||||
performSearchResultNavigation(
|
||||
index = index,
|
||||
searchState = searchState,
|
||||
|
|
@ -1613,17 +1628,36 @@ fun EpubReaderHost(
|
|||
paginator = paginator,
|
||||
coroutineScope = scope,
|
||||
onVerticalChapterChange = { chapterIdx, chunkIdx, result ->
|
||||
initialScrollTargetForChapter = ChapterScrollPosition.START
|
||||
Timber.tag("NavDiag").d("onVerticalChapterChange chapterIdx=$chapterIdx, chunkIdx=$chunkIdx, query=${result.query}")
|
||||
initialScrollTargetForChapter = null
|
||||
chunkTargetOverride = chunkIdx
|
||||
currentScrollYPosition = 0
|
||||
currentScrollHeightValue = 0
|
||||
currentChapterIndex = chapterIdx
|
||||
searchHighlightTarget = result
|
||||
loadUpToChunkIndex = chunkIdx
|
||||
},
|
||||
onVerticalScrollToResult = { _ ->
|
||||
searchHighlightTarget = null
|
||||
onVerticalScrollToResult = { result ->
|
||||
Timber.tag("NavDiag").d("onVerticalScrollToResult query=${result.query}, chunk=${result.chunkIndex}")
|
||||
val targetChunk = result.chunkIndex
|
||||
if (targetChunk >= loadedChunkCount) {
|
||||
val chunksToInject = (loadedChunkCount..targetChunk)
|
||||
chunksToInject.forEach { idx ->
|
||||
val content = chapterChunks.getOrNull(idx)
|
||||
if (content != null) {
|
||||
val escaped = escapeJsString(content)
|
||||
webViewRefForTts?.evaluateJavascript(
|
||||
"javascript:window.virtualization.appendChunk($idx, '$escaped');",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
loadUpToChunkIndex = targetChunk
|
||||
loadedChunkCount = max(loadedChunkCount, targetChunk + 1)
|
||||
}
|
||||
searchHighlightTarget = result
|
||||
},
|
||||
onPaginatedScrollToPage = { pageIdx ->
|
||||
Timber.tag("NavDiag").d("onPaginatedScrollToPage pageIdx=$pageIdx")
|
||||
paginatedPagerState.scrollToPage(pageIdx)
|
||||
}
|
||||
)
|
||||
|
|
@ -1763,23 +1797,8 @@ fun EpubReaderHost(
|
|||
Timber.tag("BookmarkDiagnosis").d("Navigating to ${bookmark.cfi}")
|
||||
cfiToLoad = bookmark.cfi
|
||||
|
||||
val directChunkIndex = try {
|
||||
val parts = bookmark.cfi.split('/').mapNotNull { it.toIntOrNull() }
|
||||
if (parts.isNotEmpty()) {
|
||||
val firstIndex = parts[0]
|
||||
(firstIndex - 2) / 2
|
||||
} else null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val locator = if (directChunkIndex == null) {
|
||||
locatorConverter.getLocatorFromCfi(epubBook, bookmark.chapterIndex, bookmark.cfi)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val targetChunk = directChunkIndex ?: locator?.let { it.blockIndex / 20 }
|
||||
val locator = locatorConverter.getLocatorFromCfi(epubBook, bookmark.chapterIndex, bookmark.cfi)
|
||||
val targetChunk = locator?.let { it.blockIndex / 20 }
|
||||
|
||||
if (bookmark.chapterIndex != currentChapterIndex) {
|
||||
chunkTargetOverride = if (targetChunk != null && targetChunk >= 0) {
|
||||
|
|
@ -2168,7 +2187,7 @@ fun EpubReaderHost(
|
|||
} else if (chapterChunks.isNotEmpty()) {
|
||||
val initialContentToLoad = remember(loadUpToChunkIndex, chapterChunks) {
|
||||
val targetIdx = loadUpToChunkIndex
|
||||
val startIdx = maxOf(0, targetIdx - 1)
|
||||
val startIdx = 0
|
||||
val endIdx = minOf(chapterChunks.lastIndex, targetIdx + 1)
|
||||
|
||||
chapterChunks.indices.joinToString(separator = "\n") { index ->
|
||||
|
|
@ -2221,30 +2240,31 @@ fun EpubReaderHost(
|
|||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(isWebViewReady) {
|
||||
LaunchedEffect(isWebViewReady, searchHighlightTarget) {
|
||||
val target = searchHighlightTarget
|
||||
Timber.d("Effect(isWebViewReady=$isWebViewReady) triggered for chapter $targetChapterIndex. Target is: $target"
|
||||
)
|
||||
Timber.tag("NavDiag").d("Effect(isWebViewReady=$isWebViewReady, target=$target) triggered for chapter $targetChapterIndex.")
|
||||
|
||||
if (isWebViewReady && target != null && target.locationInSource == targetChapterIndex) {
|
||||
Timber.d("Highlighting condition met. Highlighting now."
|
||||
)
|
||||
Timber.tag("NavDiag").d("Highlighting condition met. Highlighting now.")
|
||||
delay(200)
|
||||
val webView = webViewRefForTts
|
||||
if (webView != null) {
|
||||
val escapedQuery = escapeJsString(target.query)
|
||||
val js =
|
||||
"javascript:window.highlightAllOccurrences('${escapedQuery}'); window.scrollToOccurrence(${target.occurrenceIndexInLocation});"
|
||||
Timber.d("Executing search highlight/scroll JS: $js"
|
||||
)
|
||||
val targetChunk = target.chunkIndex
|
||||
|
||||
val relativeIdx = searchState.searchResults
|
||||
.filter { it.locationInSource == target.locationInSource && it.chunkIndex == targetChunk }
|
||||
.indexOf(target)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val js = "javascript:console.log('NavDiag: Executing robust search highlight JS'); window.CURRENT_SEARCH_QUERY = '${escapedQuery}'; window.highlightAllOccurrences('${escapedQuery}'); window.scrollToChunkOccurrence($targetChunk, $relativeIdx);"
|
||||
Timber.tag("NavDiag").d("Executing search highlight/scroll JS: $js")
|
||||
webView.evaluateJavascript(js) { result ->
|
||||
Timber.d("JS highlight/scroll result: $result"
|
||||
)
|
||||
Timber.tag("NavDiag").d("JS highlight/scroll result: $result")
|
||||
}
|
||||
searchHighlightTarget = null
|
||||
} else {
|
||||
Timber.w("Highlight failed: WebView was null even after ready signal."
|
||||
)
|
||||
Timber.tag("NavDiag").w("Highlight failed: WebView was null even after ready signal.")
|
||||
searchHighlightTarget = null
|
||||
}
|
||||
}
|
||||
|
|
@ -2305,6 +2325,7 @@ fun EpubReaderHost(
|
|||
},
|
||||
onChapterInitiallyScrolled = {
|
||||
val wasCfiScroll = cfiToLoad != null
|
||||
Timber.tag("NavDiag").d("onChapterInitiallyScrolled for chapter $targetChapterIndex. Was CFI scroll: $wasCfiScroll")
|
||||
initialScrollTargetForChapter = null
|
||||
cfiToLoad = null
|
||||
fragmentToLoad = null
|
||||
|
|
@ -3485,6 +3506,8 @@ fun EpubReaderHost(
|
|||
tapToNavigateEnabled = tapToNavigateEnabled,
|
||||
volumeScrollEnabled = volumeScrollEnabled,
|
||||
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
|
||||
hiddenTools = hiddenTools,
|
||||
onCustomizeTools = { showCustomizeToolsSheet = true },
|
||||
onNavigateBack = { triggerSaveAndExit() },
|
||||
isKeepScreenOn = isKeepScreenOn,
|
||||
onToggleKeepScreenOn = { enabled ->
|
||||
|
|
@ -3497,23 +3520,34 @@ fun EpubReaderHost(
|
|||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
containerFocusRequester.requestFocus()
|
||||
webViewRefForTts?.evaluateJavascript("javascript:window.clearSearchHighlights();", null)
|
||||
},
|
||||
onChangeRenderMode = { newMode ->
|
||||
Timber.tag("NavDiag").d("onChangeRenderMode to $newMode")
|
||||
if (newMode != currentRenderMode) {
|
||||
if (newMode == RenderMode.PAGINATED) {
|
||||
isSwitchingToPaginated = true
|
||||
webViewRefForTts?.evaluateJavascript("javascript:CfiBridge.onCfiExtracted(window.getCurrentCfi());", null)
|
||||
} else {
|
||||
scope.launch {
|
||||
Timber.tag("NavDiag").d("Mode changing to VERTICAL. lastKnownLocator=$lastKnownLocator")
|
||||
lastKnownLocator?.let { locator ->
|
||||
val cfi = locatorConverter.getCfiFromLocator(epubBook, locator)
|
||||
Timber.tag("NavDiag").d("Converted locator to CFI: $cfi")
|
||||
if (cfi != null) {
|
||||
val targetChunk = locator.blockIndex / 20
|
||||
chunkTargetOverride = targetChunk
|
||||
if (currentChapterIndex != locator.chapterIndex) {
|
||||
initialScrollTargetForChapter = null
|
||||
currentScrollYPosition = 0
|
||||
currentScrollHeightValue = 0
|
||||
currentChapterIndex = locator.chapterIndex
|
||||
} else {
|
||||
if (targetChunk > loadUpToChunkIndex) {
|
||||
loadUpToChunkIndex = targetChunk
|
||||
loadedChunkCount = max(loadedChunkCount, targetChunk + 1)
|
||||
}
|
||||
initialScrollTargetForChapter = null
|
||||
}
|
||||
cfiToLoad = cfi
|
||||
} else {
|
||||
|
|
@ -3661,7 +3695,15 @@ fun EpubReaderHost(
|
|||
saveAutoScrollUseSlider(context, autoScrollUseSlider)
|
||||
},
|
||||
isLocalMode = isAutoScrollLocal,
|
||||
onLocalModeToggle = onToggleAutoScrollMode
|
||||
onLocalModeToggle = onToggleAutoScrollMode,
|
||||
onScrollToTop = {
|
||||
if (isAutoScrollPlaying) {
|
||||
triggerAutoScrollTempPause(1000L)
|
||||
}
|
||||
scope.launch {
|
||||
webViewRefForTts?.evaluateJavascript("window.scrollTo({ top: 0, behavior: 'smooth' });", null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3672,6 +3714,7 @@ fun EpubReaderHost(
|
|||
isTtsSessionActive = isTtsSessionActive,
|
||||
ttsState = ttsState,
|
||||
isProUser = isProUser,
|
||||
hiddenTools = hiddenTools,
|
||||
onOpenSlider = {
|
||||
when (currentRenderMode) {
|
||||
RenderMode.VERTICAL_SCROLL -> {
|
||||
|
|
@ -4123,6 +4166,17 @@ fun EpubReaderHost(
|
|||
)
|
||||
}
|
||||
|
||||
if (showCustomizeToolsSheet) {
|
||||
CustomizeToolsSheet(
|
||||
hiddenTools = hiddenTools,
|
||||
onUpdate = { newHiddenSet ->
|
||||
hiddenTools = newHiddenSet
|
||||
saveHiddenTools(context, newHiddenSet)
|
||||
},
|
||||
onDismiss = { showCustomizeToolsSheet = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showDictionarySettingsSheet) {
|
||||
DictionarySettingsDialog(
|
||||
isVisible = true,
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ class OpdsParser {
|
|||
private fun parseOpds2Navigation(nav: JSONObject, baseUrl: String): OpdsEntry {
|
||||
val title = nav.optString("title", "Unknown")
|
||||
val href = nav.optString("href")
|
||||
val summary = nav.optString("description", null)
|
||||
val summary = nav.optString("description")
|
||||
val navigationUrl = if (href.isNotEmpty()) resolveUrl(baseUrl, href) else null
|
||||
|
||||
return OpdsEntry(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,15 @@ class OpdsRepository(context: Context) {
|
|||
private const val KEY_CATALOGS_JSON = "opds_catalogs_json"
|
||||
|
||||
val sharedHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder().build()
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
val originalRequest = chain.request()
|
||||
val requestWithUserAgent = originalRequest.newBuilder()
|
||||
.header("User-Agent", "EpistemeReader/1.0 (Android)")
|
||||
.build()
|
||||
chain.proceed(requestWithUserAgent)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +62,8 @@ class OpdsRepository(context: Context) {
|
|||
|
||||
if (catalogs.isEmpty()) {
|
||||
catalogs.add(OpdsCatalog(UUID.randomUUID().toString(), "Project Gutenberg", "https://m.gutenberg.org/ebooks.opds/", isDefault = true))
|
||||
catalogs.add(OpdsCatalog(UUID.randomUUID().toString(), "Standard Ebooks", "https://standardebooks.org/feeds/opds", isDefault = true))
|
||||
|
||||
saveCatalogs(catalogs)
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -123,7 +123,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -1104,12 +1104,12 @@ internal fun PdfPageComposable(
|
|||
try {
|
||||
page = withContext(Dispatchers.IO) { pdfDocumentItem.openPage(pdfPageIndex) }
|
||||
|
||||
snapshotFlow { visibleScreenRect() }.collectLatest { currentVisibleRect ->
|
||||
snapshotFlow { visibleScreenRect() }.conflate().collect { currentVisibleRect ->
|
||||
val tileCalcStart = System.nanoTime()
|
||||
if (!isActive) return@collectLatest
|
||||
if (!isActive) return@collect
|
||||
|
||||
if (isScrolling && effectiveScale > 1f) {
|
||||
return@collectLatest
|
||||
return@collect
|
||||
}
|
||||
|
||||
val pxTl: Float
|
||||
|
|
@ -1131,7 +1131,7 @@ internal fun PdfPageComposable(
|
|||
oldTiles.forEach { PdfBitmapPool.recycle(it.bitmap) }
|
||||
}
|
||||
}
|
||||
return@collectLatest
|
||||
return@collect
|
||||
}
|
||||
} else {
|
||||
val pivotX = screenWidth / 2f
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ class VerticalPdfReaderState {
|
|||
internal var scrollToPageHandler: (suspend (Int) -> Unit)? = null
|
||||
internal var snapToPageHandler: (suspend (Int) -> Unit)? = null
|
||||
internal var scrollByHandler: (suspend (Float) -> Unit)? = null
|
||||
internal var scrollToTopHandler: (suspend () -> Unit)? = null
|
||||
internal var scrollToBottomHandler: (suspend () -> Unit)? = null
|
||||
|
||||
suspend fun scrollToPage(pageIndex: Int) {
|
||||
scrollToPageHandler?.invoke(pageIndex)
|
||||
|
|
@ -151,6 +153,14 @@ class VerticalPdfReaderState {
|
|||
suspend fun scrollBy(delta: Float) {
|
||||
scrollByHandler?.invoke(delta)
|
||||
}
|
||||
|
||||
suspend fun scrollToTop() {
|
||||
scrollToTopHandler?.invoke()
|
||||
}
|
||||
|
||||
suspend fun scrollToBottom() {
|
||||
scrollToBottomHandler?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -244,6 +254,8 @@ internal fun PdfVerticalReader(
|
|||
state.scrollToPageHandler = null
|
||||
state.snapToPageHandler = null
|
||||
state.scrollByHandler = null
|
||||
state.scrollToTopHandler = null
|
||||
state.scrollToBottomHandler = null
|
||||
}
|
||||
}
|
||||
var globalEraserPosition by remember { mutableStateOf<Offset?>(null) }
|
||||
|
|
@ -527,6 +539,23 @@ internal fun PdfVerticalReader(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.scrollToTopHandler = {
|
||||
panYAnimatable.animateTo(
|
||||
targetValue = headerHeightPx,
|
||||
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing)
|
||||
)
|
||||
}
|
||||
|
||||
state.scrollToBottomHandler = {
|
||||
val currentZoom = zoomAnimatable.value
|
||||
val zoomedDocHeight = totalDocHeight * currentZoom
|
||||
val minPanY = (screenHeight - footerHeightPx - zoomedDocHeight).coerceAtMost(headerHeightPx)
|
||||
panYAnimatable.animateTo(
|
||||
targetValue = minPanY,
|
||||
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var selectionClearTrigger by remember { mutableLongStateOf(0L) }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
// PdfViewerScreen.kt
|
||||
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH", "Unused", "UnusedVariable",
|
||||
"SimplifyBooleanWithConstants"
|
||||
)
|
||||
) @file:kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package com.aryan.reader.pdf
|
||||
|
||||
|
|
@ -30,6 +30,8 @@ import android.app.Activity
|
|||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
|
|
@ -91,6 +93,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -364,6 +367,42 @@ private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package"
|
|||
private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
|
||||
private const val PDF_THEME_KEY = "pdf_reader_theme"
|
||||
private const val PDF_KEEP_SCREEN_ON_KEY = "pdf_keep_screen_on_enabled"
|
||||
private const val PDF_HIDDEN_TOOLS_KEY = "pdf_hidden_tools"
|
||||
|
||||
private fun loadPdfHiddenTools(context: Context): Set<String> {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getStringSet(PDF_HIDDEN_TOOLS_KEY, emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
private fun savePdfHiddenTools(context: Context, hiddenTools: Set<String>) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putStringSet(PDF_HIDDEN_TOOLS_KEY, hiddenTools) }
|
||||
}
|
||||
|
||||
enum class PdfReaderTool(val title: String, val category: String) {
|
||||
DICTIONARY("External Apps", "Top Bar"),
|
||||
THEME("Theme Settings", "Top Bar"),
|
||||
LOCK_PANNING("Lock Panning", "Top Bar"),
|
||||
FULL_SCREEN("Full Screen", "Top Bar"),
|
||||
SLIDER("Navigation Slider", "Bottom Bar"),
|
||||
TOC("Sidebar", "Bottom Bar"),
|
||||
SEARCH("Search", "Bottom Bar"),
|
||||
HIGHLIGHT_ALL("Highlight selectable text", "Bottom Bar"),
|
||||
AI_FEATURES("AI Features", "Bottom Bar"),
|
||||
EDIT_MODE("Edit Mode", "Bottom Bar"),
|
||||
TTS_CONTROLS("TTS Controls", "Bottom Bar"),
|
||||
OCR_LANGUAGE("OCR Language", "Overflow Menu"),
|
||||
READING_MODE("Reading Mode", "Overflow Menu"),
|
||||
KEEP_SCREEN_ON("Keep Screen On", "Overflow Menu"),
|
||||
AUTO_SCROLL("Auto Scroll", "Overflow Menu"),
|
||||
TTS_SETTINGS("TTS Voice Settings", "Overflow Menu"),
|
||||
BOOKMARK("Bookmark", "Overflow Menu"),
|
||||
PAGE_MANAGEMENT("Page Management", "Overflow Menu"),
|
||||
REFLOW("Text View (Reflow)", "Overflow Menu"),
|
||||
SHARE("Share", "Overflow Menu"),
|
||||
SAVE_COPY("Save Copy", "Overflow Menu"),
|
||||
PRINT("Print", "Overflow Menu")
|
||||
}
|
||||
|
||||
private fun loadCustomHighlightColors(context: Context): Map<PdfHighlightColor, Color> {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
|
@ -1213,6 +1252,14 @@ fun PdfViewerScreen(
|
|||
var showReindexDialog by remember { mutableStateOf<OcrLanguage?>(null) }
|
||||
var pendingActionAfterOcrSelection by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
|
||||
var showCustomizeToolsSheet by remember { mutableStateOf(false) }
|
||||
var hiddenTools by remember { mutableStateOf(loadPdfHiddenTools(context)) }
|
||||
|
||||
val onUpdateHiddenTools = { newSet: Set<String> ->
|
||||
hiddenTools = newSet
|
||||
savePdfHiddenTools(context, newSet)
|
||||
}
|
||||
|
||||
val executeWithOcrCheck = remember(hasSelectedOcrLanguage) {
|
||||
{ action: () -> Unit ->
|
||||
if (hasSelectedOcrLanguage) {
|
||||
|
|
@ -3815,6 +3862,7 @@ fun PdfViewerScreen(
|
|||
showSummarizationUpsellDialog -> showSummarizationUpsellDialog = false
|
||||
showAiDefinitionPopup -> showAiDefinitionPopup = false
|
||||
showDictionaryUpsellDialog -> showDictionaryUpsellDialog = false
|
||||
showCustomizeToolsSheet -> showCustomizeToolsSheet = false
|
||||
isPageSliderVisible -> {
|
||||
isPageSliderVisible = false
|
||||
showBars = true
|
||||
|
|
@ -5143,7 +5191,7 @@ fun PdfViewerScreen(
|
|||
triggerAutoScrollTempPause(1000L)
|
||||
|
||||
coroutineScope.launch {
|
||||
verticalReaderState.scrollToPage(0)
|
||||
verticalReaderState.scrollToTop()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -5210,7 +5258,7 @@ fun PdfViewerScreen(
|
|||
triggerAutoScrollTempPause(1000L)
|
||||
|
||||
coroutineScope.launch {
|
||||
verticalReaderState.scrollToPage(totalPages - 1)
|
||||
verticalReaderState.scrollToBottom()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -5555,6 +5603,7 @@ fun PdfViewerScreen(
|
|||
.testTag("PageNumberIndicator")
|
||||
)
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.THEME.name)) {
|
||||
TooltipIconButton(
|
||||
text = "Theme",
|
||||
description = "Theme Settings",
|
||||
|
|
@ -5565,7 +5614,9 @@ fun PdfViewerScreen(
|
|||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.LOCK_PANNING.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan)
|
||||
else stringResource(R.string.tooltip_lock_pan),
|
||||
|
|
@ -5585,7 +5636,9 @@ fun PdfViewerScreen(
|
|||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.FULL_SCREEN.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_fullscreen),
|
||||
description = stringResource(R.string.tooltip_fullscreen_desc),
|
||||
|
|
@ -5599,7 +5652,9 @@ fun PdfViewerScreen(
|
|||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.DICTIONARY.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
|
|
@ -5610,6 +5665,7 @@ fun PdfViewerScreen(
|
|||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
TooltipIconButton(
|
||||
|
|
@ -5682,7 +5738,18 @@ fun PdfViewerScreen(
|
|||
DropdownMenu(
|
||||
expanded = showMoreMenu,
|
||||
onDismissRequest = { showMoreMenu = false }) {
|
||||
if (BuildConfig.IS_PRO) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Customize Toolbar") },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
showCustomizeToolsSheet = true
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
if (BuildConfig.IS_PRO && !hiddenTools.contains(PdfReaderTool.OCR_LANGUAGE.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("OCR Language") },
|
||||
onClick = {
|
||||
|
|
@ -5693,6 +5760,7 @@ fun PdfViewerScreen(
|
|||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (!hiddenTools.contains(PdfReaderTool.READING_MODE.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Reading Mode: Vertical scroll") },
|
||||
enabled = !isTtsSessionActive,
|
||||
|
|
@ -5725,6 +5793,8 @@ fun PdfViewerScreen(
|
|||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.KEEP_SCREEN_ON.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Keep Screen On") },
|
||||
onClick = {
|
||||
|
|
@ -5741,6 +5811,8 @@ fun PdfViewerScreen(
|
|||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.AUTO_SCROLL.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Auto Scroll") },
|
||||
enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL,
|
||||
|
|
@ -5752,6 +5824,8 @@ fun PdfViewerScreen(
|
|||
})
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.TTS_SETTINGS.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("TTS Voice Settings") },
|
||||
onClick = {
|
||||
|
|
@ -5764,7 +5838,8 @@ fun PdfViewerScreen(
|
|||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
DropdownMenuItem(
|
||||
|
|
@ -5779,10 +5854,12 @@ fun PdfViewerScreen(
|
|||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.BOOKMARK.name)) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (isBookmarked) "Remove bookmark"
|
||||
|
|
@ -5793,7 +5870,8 @@ fun PdfViewerScreen(
|
|||
onBookmarkClick()
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.PAGE_MANAGEMENT.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Insert Blank Page") },
|
||||
onClick = {
|
||||
|
|
@ -5815,9 +5893,9 @@ fun PdfViewerScreen(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.REFLOW.name)) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
|
|
@ -5850,8 +5928,7 @@ fun PdfViewerScreen(
|
|||
uiState.allRecentFiles.find { it.bookId == reflowBookId }
|
||||
if (item != null) {
|
||||
viewModel.switchToFileSeamlessly(
|
||||
item,
|
||||
resolvedPage
|
||||
item, resolvedPage
|
||||
)
|
||||
} else {
|
||||
viewModel.generateAndImportReflowFile(
|
||||
|
|
@ -5878,18 +5955,24 @@ fun PdfViewerScreen(
|
|||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
})
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(text = { Text("Share") }, onClick = {
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.SHARE.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Share") },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
showShareDialog = true
|
||||
}, leadingIcon = {
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Share, contentDescription = null
|
||||
Icons.Default.Share,
|
||||
contentDescription = null
|
||||
)
|
||||
})
|
||||
if (uiState.selectedFileType == FileType.PDF) {
|
||||
}
|
||||
)
|
||||
}
|
||||
if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.SAVE_COPY.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Save copy to device") },
|
||||
onClick = {
|
||||
|
|
@ -5903,7 +5986,7 @@ fun PdfViewerScreen(
|
|||
)
|
||||
})
|
||||
}
|
||||
if (uiState.selectedFileType == FileType.PDF) {
|
||||
if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.PRINT.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Print") },
|
||||
onClick = {
|
||||
|
|
@ -6247,11 +6330,13 @@ fun PdfViewerScreen(
|
|||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
// Slider Navigation Trigger
|
||||
if (!hiddenTools.contains(PdfReaderTool.SLIDER.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = {
|
||||
val currentPage = if (displayMode == DisplayMode.PAGINATION) {
|
||||
val currentPage =
|
||||
if (displayMode == DisplayMode.PAGINATION) {
|
||||
pagerState.currentPage
|
||||
} else {
|
||||
verticalReaderState.currentPage
|
||||
|
|
@ -6260,14 +6345,16 @@ fun PdfViewerScreen(
|
|||
sliderCurrentPage = currentPage.toFloat()
|
||||
isPageSliderVisible = true
|
||||
showBars = false
|
||||
}, enabled = !(ttsState.isPlaying || ttsState.isLoading)
|
||||
},
|
||||
enabled = !(ttsState.isPlaying || ttsState.isLoading)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.slider),
|
||||
contentDescription = "Navigate with slider"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.TOC.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
|
|
@ -6280,8 +6367,10 @@ fun PdfViewerScreen(
|
|||
contentDescription = "Table of Contents"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Search Button
|
||||
if (!hiddenTools.contains(PdfReaderTool.SEARCH.name)) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
|
|
@ -6299,16 +6388,13 @@ fun PdfViewerScreen(
|
|||
contentDescription = "Search"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if (!hiddenTools.contains(PdfReaderTool.HIGHLIGHT_ALL.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (showAllTextHighlights)
|
||||
stringResource(R.string.tooltip_highlights_off)
|
||||
else
|
||||
stringResource(R.string.tooltip_highlights),
|
||||
description = if (showAllTextHighlights)
|
||||
stringResource(R.string.tooltip_highlights_off_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_highlights_desc),
|
||||
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off)
|
||||
else stringResource(R.string.tooltip_highlights),
|
||||
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc)
|
||||
else stringResource(R.string.tooltip_highlights_desc),
|
||||
onClick = {
|
||||
val newState = !showAllTextHighlights
|
||||
if (newState) {
|
||||
|
|
@ -6331,9 +6417,10 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI feat
|
||||
if (BuildConfig.FLAVOR != "oss") {
|
||||
if (BuildConfig.FLAVOR != "oss" && !hiddenTools.contains(PdfReaderTool.AI_FEATURES.name)) {
|
||||
Box {
|
||||
var showAiFeaturesMenu by remember { mutableStateOf(false) }
|
||||
TooltipIconButton(
|
||||
|
|
@ -6375,20 +6462,19 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
// Edit Button
|
||||
if (!hiddenTools.contains(PdfReaderTool.EDIT_MODE.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (isEditMode)
|
||||
stringResource(R.string.tooltip_edit_mode_exit)
|
||||
else
|
||||
stringResource(R.string.tooltip_edit_mode),
|
||||
description = if (isEditMode)
|
||||
stringResource(R.string.tooltip_edit_mode_exit_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_edit_mode_desc),
|
||||
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit)
|
||||
else stringResource(R.string.tooltip_edit_mode),
|
||||
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc)
|
||||
else stringResource(R.string.tooltip_edit_mode_desc),
|
||||
onClick = {
|
||||
val newEditMode = !isEditMode
|
||||
val currentActivePage = richTextController?.activePageIndex ?: -1
|
||||
val currentActivePage =
|
||||
richTextController?.activePageIndex ?: -1
|
||||
|
||||
Timber.tag("RichTextMigration").i("Edit Toggle: $isEditMode -> $newEditMode (ActivePage: $currentActivePage)")
|
||||
Timber.tag("RichTextMigration")
|
||||
.i("Edit Toggle: $isEditMode -> $newEditMode (ActivePage: $currentActivePage)")
|
||||
|
||||
if (!newEditMode && richTextController != null) {
|
||||
coroutineScope.launch {
|
||||
|
|
@ -6401,25 +6487,22 @@ fun PdfViewerScreen(
|
|||
|
||||
isEditMode = newEditMode
|
||||
if (!newEditMode) showBars = true
|
||||
}
|
||||
) {
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Toggle Editing Mode",
|
||||
tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TTS
|
||||
if (!hiddenTools.contains(PdfReaderTool.TTS_CONTROLS.name)) {
|
||||
TooltipIconButton(
|
||||
text = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start_desc),
|
||||
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop)
|
||||
else stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc)
|
||||
else stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = {
|
||||
if (isTtsSessionActive) {
|
||||
Timber.d("TTS button clicked: Stopping TTS")
|
||||
|
|
@ -6436,6 +6519,7 @@ fun PdfViewerScreen(
|
|||
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TTS Pause/Resume Button
|
||||
if (isTtsSessionActive) {
|
||||
|
|
@ -7759,7 +7843,22 @@ fun PdfViewerScreen(
|
|||
savePdfAutoScrollUseSlider(context, autoScrollUseSlider)
|
||||
},
|
||||
isLocalMode = isAutoScrollLocal,
|
||||
onLocalModeToggle = onToggleAutoScrollMode
|
||||
onLocalModeToggle = onToggleAutoScrollMode,
|
||||
onScrollToTop = {
|
||||
if (isAutoScrollPlaying) {
|
||||
triggerAutoScrollTempPause(1000L)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
verticalReaderState.scrollToTop()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showCustomizeToolsSheet) {
|
||||
PdfCustomizeToolsSheet(
|
||||
hiddenTools = hiddenTools,
|
||||
onUpdate = onUpdateHiddenTools,
|
||||
onDismiss = { showCustomizeToolsSheet = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8412,3 +8511,74 @@ fun PdfSearchResultsList(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PdfCustomizeToolsSheet(
|
||||
hiddenTools: Set<String>,
|
||||
onUpdate: (Set<String>) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
contentWindowInsets = { WindowInsets.navigationBars }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) {
|
||||
Text(
|
||||
text = "Customize Toolbar",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Select the tools you want to keep visible. Unchecking a tool hides it from the UI to give you a distraction-free reading space.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
PdfReaderTool.entries.groupBy { it.category }.forEach { (category, tools) ->
|
||||
item {
|
||||
Text(
|
||||
text = category,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
items(tools) { tool ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable {
|
||||
val newSet = hiddenTools.toMutableSet()
|
||||
if (newSet.contains(tool.name)) newSet.remove(tool.name)
|
||||
else newSet.add(tool.name)
|
||||
onUpdate(newSet)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = tool.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Switch(
|
||||
checked = !hiddenTools.contains(tool.name),
|
||||
onCheckedChange = { isVisible ->
|
||||
val newSet = hiddenTools.toMutableSet()
|
||||
if (isVisible) newSet.remove(tool.name) else newSet.add(tool.name)
|
||||
onUpdate(newSet)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -420,7 +420,7 @@ class PdfRichTextRepository(private val context: Context) {
|
|||
isItalic = sObj.optBoolean("i"),
|
||||
isUnderline = sObj.optBoolean("u"),
|
||||
isStrikethrough = sObj.optBoolean("st"),
|
||||
fontPath = sObj.optString("fp", null),
|
||||
fontPath = sObj.optString("fp"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,8 +200,8 @@ object TextBoxSerializer {
|
|||
isItalic = obj.optBoolean("isItalic", false),
|
||||
isUnderline = obj.optBoolean("isUnderline", false),
|
||||
isStrikeThrough = obj.optBoolean("isStrikeThrough", false),
|
||||
fontPath = obj.optString("fontPath", null).takeIf { !it.isNullOrBlank() },
|
||||
fontName = obj.optString("fontName", null).takeIf { !it.isNullOrBlank() }
|
||||
fontPath = obj.optString("fontPath").takeIf { !it.isNullOrBlank() },
|
||||
fontName = obj.optString("fontName").takeIf { !it.isNullOrBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -269,7 +269,7 @@ object HighlightSerializer {
|
|||
color = try { PdfHighlightColor.valueOf(obj.getString("color")) } catch(_: Exception) { PdfHighlightColor.YELLOW },
|
||||
text = obj.optString("text", ""),
|
||||
range = Pair(obj.optInt("rangeStart", 0), obj.optInt("rangeEnd", 0)),
|
||||
note = obj.optString("note", null).takeIf { !it.isNullOrBlank() }
|
||||
note = obj.optString("note").takeIf { !it.isNullOrBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,18 @@
|
|||
<string name="filter_source_folder">Source Folder</string>
|
||||
<string name="filter_read_status">Read Status</string>
|
||||
<string name="clear_all">Clear All</string>
|
||||
<string name="filter_in_app_storage">In-App Storage</string>
|
||||
|
||||
<!-- External File Behavior -->
|
||||
<string name="external_file_prompt_title">Save File?</string>
|
||||
<string name="external_file_prompt_desc">Do you want to save this external file in the app\'s library? If not, it will be removed.\n\n(You can change this default behavior anytime from the Home Screen > More Options > External File Behavior).</string>
|
||||
<string name="external_file_dont_ask">Don\'t ask again</string>
|
||||
<string name="external_file_keep">Keep in Library</string>
|
||||
<string name="external_file_delete">Remove</string>
|
||||
<string name="external_file_behavior_ask">Ask Every Time</string>
|
||||
<string name="external_file_behavior_keep">Always Keep</string>
|
||||
<string name="external_file_behavior_delete">Always Remove</string>
|
||||
<string name="options_external_file_behavior">External File Behavior</string>
|
||||
|
||||
<!-- OPDS -->
|
||||
<string name="fab_add_catalog">Add Catalog</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue