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:
Aryan 2026-04-10 17:30:48 +05:30 committed by GitHub
parent db7e05ce63
commit 6a60aec0ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 3045 additions and 2060 deletions

View file

@ -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()
@ -1284,4 +1308,78 @@ 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)) }
}
)
}