General improvements (#316)

* Added support for right text alignment in epub reader

* Added tabs management to PDF navigation drawer

* Implemented unified selection menu placement logic

* Added reset functionality for toolbar customization

* Improved highlight filtering in epub pagination

* Migrated hardcoded UI strings to string resources

* Extracted desktop application logic from Main.kt into modular files

* Refactored Main.kt by extracting PDF and EPUB logic into specialized files

* Added Spanish language support

* Added system default option to app language selection
This commit is contained in:
Aryan 2026-05-16 16:19:31 +05:30 committed by GitHub
parent 759d4b73a0
commit 056485a140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 9184 additions and 5947 deletions

View file

@ -29,6 +29,9 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
@ -40,6 +43,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ScrollableTabRow
@ -58,6 +62,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringResource
@ -76,6 +81,7 @@ import kotlinx.coroutines.withContext
import org.json.JSONArray
import timber.log.Timber
import androidx.core.graphics.createBitmap
import com.aryan.reader.data.RecentFileItem
import com.aryan.reader.pdf.data.VirtualPage
private const val MAX_FIXED_RECURSION = 128
@ -84,6 +90,32 @@ internal data class PdfBookmark(val pageIndex: Int, val title: String, val total
internal data class TocEntry(val title: String, val pageIndex: Int, val nestLevel: Int)
private enum class PdfDrawerSection {
TABS,
CHAPTERS,
BOOKMARKS,
HIGHLIGHTS,
PAGES
}
private val PdfDrawerSection.titleResId: Int
get() = when (this) {
PdfDrawerSection.TABS -> R.string.tab_tabs
PdfDrawerSection.CHAPTERS -> R.string.tab_chapters
PdfDrawerSection.BOOKMARKS -> R.string.tab_bookmarks
PdfDrawerSection.HIGHLIGHTS -> R.string.tab_highlights
PdfDrawerSection.PAGES -> R.string.tab_pages
}
private val PdfDrawerSection.testTag: String?
get() = when (this) {
PdfDrawerSection.TABS -> "TabsTab"
PdfDrawerSection.BOOKMARKS -> "BookmarksTab"
PdfDrawerSection.HIGHLIGHTS -> "HighlightsTab"
PdfDrawerSection.PAGES -> "PagesTab"
PdfDrawerSection.CHAPTERS -> null
}
/**
* Patches the library bug where siblings are truncated due to depth-state leakage.
*/
@ -278,6 +310,225 @@ internal fun PdfTocTreeItem(
}
}
@Composable
private fun PdfTabsDrawerPage(
openTabs: List<RecentFileItem>,
activeTabBookId: String?,
currentPage: Int,
totalPages: Int,
onTabSelected: (String) -> Unit,
onTabClosed: (String) -> Unit,
onNewTabClick: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, top = 10.dp, end = 8.dp, bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.active_tabs),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.weight(1f)
)
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) {
Text(
text = openTabs.size.toString(),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
)
}
IconButton(
onClick = onNewTabClick,
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(R.string.content_desc_new_tab)
)
}
}
HorizontalDivider()
if (openTabs.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize().padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.msg_no_other_pdfs_found),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(openTabs, key = { it.bookId }) { tab ->
PdfDrawerTabItem(
tab = tab,
isSelected = tab.bookId == activeTabBookId,
currentPage = currentPage,
totalPages = totalPages,
onTabSelected = onTabSelected,
onTabClosed = onTabClosed
)
}
}
}
}
}
@Composable
private fun PdfDrawerTabItem(
tab: RecentFileItem,
isSelected: Boolean,
currentPage: Int,
totalPages: Int,
onTabSelected: (String) -> Unit,
onTabClosed: (String) -> Unit
) {
val shape = RoundedCornerShape(8.dp)
val containerColor by animateColorAsState(
targetValue = if (isSelected) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.7f)
} else {
MaterialTheme.colorScheme.surface
},
label = "PdfDrawerTabContainer"
)
val borderColor by animateColorAsState(
targetValue = if (isSelected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.45f)
} else {
MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)
},
label = "PdfDrawerTabBorder"
)
val contentColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurface
}
val progressPercent = remember(isSelected, currentPage, totalPages, tab.progressPercentage) {
when {
isSelected && totalPages > 0 -> (((currentPage + 1).toFloat() / totalPages.toFloat()) * 100f)
.coerceIn(0f, 100f)
.toInt()
else -> tab.progressPercentage
?.coerceIn(0f, 100f)
?.toInt()
}
}
val pageLabel = when {
isSelected && totalPages > 0 -> stringResource(R.string.page_of_pages, currentPage + 1, totalPages)
tab.lastPage != null -> stringResource(R.string.pdf_page_short, tab.lastPage + 1)
else -> null
}
val progressLabel = progressPercent
?.takeIf { it > 0 }
?.let { stringResource(R.string.progress_complete, it) }
val supportingText = remember(pageLabel, progressLabel, tab.author) {
listOfNotNull(pageLabel, progressLabel, tab.author)
.distinct()
.joinToString(" - ")
}
Column(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 4.dp)
.clip(shape)
.background(containerColor)
.border(1.dp, borderColor, shape)
.clickable { onTabSelected(tab.bookId) }
.testTag("PdfDrawerTab_${tab.bookId}")
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 68.dp)
.padding(start = 12.dp, end = 6.dp, top = 10.dp, bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(
if (isSelected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.16f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
}
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Description,
contentDescription = null,
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = tab.customName ?: tab.title ?: tab.displayName,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
color = contentColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
if (supportingText.isNotBlank()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = supportingText,
style = MaterialTheme.typography.bodySmall,
color = contentColor.copy(alpha = 0.72f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
IconButton(
onClick = { onTabClosed(tab.bookId) },
modifier = Modifier.size(36.dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close_tab),
tint = contentColor.copy(alpha = 0.8f)
)
}
}
progressPercent
?.takeIf { it > 0 }
?.let { percent ->
LinearProgressIndicator(
progress = { percent / 100f },
modifier = Modifier.fillMaxWidth().height(3.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
trackColor = Color.Transparent
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun PdfNavigationDrawerContent(
@ -288,58 +539,81 @@ internal fun PdfNavigationDrawerContent(
userHighlights: List<PdfUserHighlight>,
currentPage: Int,
totalPages: Int,
isTabsEnabled: Boolean = false,
openTabs: List<RecentFileItem> = emptyList(),
activeTabBookId: String? = null,
customHighlightColors: Map<PdfHighlightColor, Color>,
onPageSelected: (Int) -> Unit,
onTabSelected: (String) -> Unit = {},
onTabClosed: (String) -> Unit = {},
onNewTabClick: () -> Unit = {},
onRenameBookmark: (PdfBookmark, String) -> Unit,
onDeleteBookmark: (PdfBookmark) -> Unit,
onDeleteHighlight: (PdfUserHighlight) -> Unit,
onNoteRequested: (String?) -> Unit,
onCloseDrawer: () -> Unit
) {
val drawerPagerState = rememberPagerState(pageCount = { 4 })
val showTabsPane = isTabsEnabled && openTabs.isNotEmpty()
val drawerSections = remember(showTabsPane) {
buildList {
if (showTabsPane) add(PdfDrawerSection.TABS)
add(PdfDrawerSection.CHAPTERS)
add(PdfDrawerSection.BOOKMARKS)
add(PdfDrawerSection.HIGHLIGHTS)
add(PdfDrawerSection.PAGES)
}
}
val drawerPagerState = rememberPagerState(pageCount = { drawerSections.size })
val drawerScope = rememberCoroutineScope()
LaunchedEffect(drawerSections.size) {
if (drawerPagerState.currentPage >= drawerSections.size) {
drawerPagerState.scrollToPage(drawerSections.lastIndex.coerceAtLeast(0))
}
}
Column(modifier = Modifier.fillMaxSize()) {
val selectedDrawerTabIndex = drawerPagerState.currentPage.coerceIn(0, drawerSections.lastIndex)
ScrollableTabRow(
selectedTabIndex = drawerPagerState.currentPage,
selectedTabIndex = selectedDrawerTabIndex,
edgePadding = 8.dp,
modifier = Modifier.fillMaxWidth()
) {
Tab(selected = drawerPagerState.currentPage == 0, onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(0) }
}, text = { Text(stringResource(R.string.tab_chapters)) })
Tab(
selected = drawerPagerState.currentPage == 1,
onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(1) }
},
text = { Text(stringResource(R.string.tab_bookmarks)) },
modifier = Modifier.testTag("BookmarksTab")
)
Tab(
selected = drawerPagerState.currentPage == 2,
onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(2) }
},
text = { Text(stringResource(R.string.tab_highlights)) },
modifier = Modifier.testTag("HighlightsTab")
)
Tab(
selected = drawerPagerState.currentPage == 3,
onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(3) }
},
text = { Text(stringResource(R.string.tab_pages)) },
modifier = Modifier.testTag("PagesTab")
)
drawerSections.forEachIndexed { index, section ->
Tab(
selected = selectedDrawerTabIndex == index,
onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(index) }
},
text = { Text(stringResource(section.titleResId)) },
modifier = section.testTag?.let { Modifier.testTag(it) } ?: Modifier
)
}
}
HorizontalPager(
state = drawerPagerState,
modifier = Modifier.fillMaxWidth().weight(1f)
) { page ->
when (page) {
0 -> { // Chapters Page
when (drawerSections[page]) {
PdfDrawerSection.TABS -> PdfTabsDrawerPage(
openTabs = openTabs,
activeTabBookId = activeTabBookId,
currentPage = currentPage,
totalPages = totalPages,
onTabSelected = { bookId ->
if (bookId == activeTabBookId) {
onCloseDrawer()
} else {
onCloseDrawer()
onTabSelected(bookId)
}
},
onTabClosed = onTabClosed,
onNewTabClick = onNewTabClick
)
PdfDrawerSection.CHAPTERS -> { // Chapters Page
if (flatTableOfContents.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize().padding(16.dp),
@ -500,7 +774,7 @@ internal fun PdfNavigationDrawerContent(
}
}
1 -> { // Bookmarks Page
PdfDrawerSection.BOOKMARKS -> { // Bookmarks Page
if (bookmarks.isEmpty()) {
Box(
modifier = Modifier
@ -642,7 +916,7 @@ internal fun PdfNavigationDrawerContent(
}
}
}
2 -> { // Highlights Page
PdfDrawerSection.HIGHLIGHTS -> { // Highlights Page
if (userHighlights.isEmpty()) {
Box(
modifier = Modifier
@ -801,7 +1075,7 @@ internal fun PdfNavigationDrawerContent(
}
}
}
3 -> { // Pages Page
PdfDrawerSection.PAGES -> { // Pages Page
val listState = rememberLazyListState()
val pageRows = remember(totalPages) { (0 until totalPages).chunked(3) }

View file

@ -79,6 +79,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -236,6 +237,9 @@ internal fun PdfSelectionMenuPopup(
onNote: (() -> Unit)? = null
) {
val context = LocalContext.current
val configuration = LocalConfiguration.current
val selectionMenuMaxHeight = (configuration.screenHeightDp.dp - 32.dp).coerceAtLeast(160.dp)
val menuScrollState = rememberScrollState()
Popup(
popupPositionProvider = popupPositionProvider,
@ -251,9 +255,15 @@ internal fun PdfSelectionMenuPopup(
shadowElevation = 8.dp,
color = MaterialTheme.colorScheme.surface,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
modifier = Modifier.widthIn(max = 300.dp)
modifier = Modifier
.widthIn(max = 280.dp)
.heightIn(max = selectionMenuMaxHeight)
) {
Column(modifier = if (menuState.isComment) Modifier.fillMaxWidth() else Modifier.width(IntrinsicSize.Max)) {
Column(
modifier = (if (menuState.isComment) Modifier.fillMaxWidth() else Modifier.width(IntrinsicSize.Max))
.heightIn(max = selectionMenuMaxHeight)
.verticalScroll(menuScrollState)
) {
if (!menuState.note.isNullOrBlank()) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
@ -330,7 +340,7 @@ internal fun PdfSelectionMenuPopup(
}
} else {
Row(
modifier = Modifier.padding(vertical = 12.dp, horizontal = 12.dp)
modifier = Modifier.padding(vertical = 8.dp, horizontal = 10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
@ -338,7 +348,7 @@ internal fun PdfSelectionMenuPopup(
PdfHighlightColor.entries.forEach { colorEnum ->
val displayColor = customHighlightColors[colorEnum] ?: colorEnum.color
Box(
modifier = Modifier.padding(horizontal = 6.dp).size(32.dp)
modifier = Modifier.padding(horizontal = 4.dp).size(28.dp)
.background(displayColor, CircleShape).clip(CircleShape)
.clickable {
Timber.tag("PdfHighlightDebug")
@ -352,8 +362,8 @@ internal fun PdfSelectionMenuPopup(
)
Box(
modifier = Modifier
.padding(horizontal = 6.dp)
.size(32.dp)
.padding(horizontal = 4.dp)
.size(28.dp)
.clip(CircleShape)
.background(Brush.sweepGradient(rainbowColors))
.clickable { onPaletteClick() },
@ -392,7 +402,7 @@ internal fun PdfSelectionMenuPopup(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp),
.padding(horizontal = 6.dp, vertical = 3.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
@ -400,22 +410,22 @@ internal fun PdfSelectionMenuPopup(
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
Column(
modifier = Modifier
.width(64.dp)
.width(56.dp)
.clickable { action.onClick() }
.padding(vertical = 8.dp),
.padding(vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (action.imageVector != null) {
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(22.dp))
} else if (action.iconRes != null) {
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(22.dp))
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
}
}
repeat(3 - rowActions.size) {
Spacer(modifier = Modifier.width(64.dp))
Spacer(modifier = Modifier.width(56.dp))
}
}
}

View file

@ -131,6 +131,10 @@ import com.aryan.reader.pdf.data.PdfTextBox
import com.aryan.reader.pdf.data.VirtualPage
import com.aryan.reader.pdf.ocr.OcrElement
import com.aryan.reader.pdf.ocr.OcrResult
import com.aryan.reader.shared.ui.SharedSelectionMenuRect
import com.aryan.reader.shared.ui.SharedSelectionMenuSize
import com.aryan.reader.shared.ui.SharedSelectionMenuViewport
import com.aryan.reader.shared.ui.sharedSelectionMenuPlacement
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
@ -2139,7 +2143,7 @@ internal fun PdfPageComposable(
val dragEventChannel = Channel<Offset>(Channel.CONFLATED)
coroutineScope.launch(Dispatchers.IO) {
val dragWorker = coroutineScope.launch(Dispatchers.IO) {
var pageForDrag: ReaderPage? = null
var textPageForDrag: ReaderTextPage? = null
@ -2473,106 +2477,107 @@ internal fun PdfPageComposable(
} finally {
dragEventChannel.close()
}
dragWorker.invokeOnCompletion {
coroutineScope.launch {
if (selectionMethodUsed == PdfSelectionMethod.PDFIUM) {
if (selectionCharRange.value != null && selectedWordScreenRects.isNotEmpty()) {
val currentRange = selectionCharRange.value!!
var pageForMenu: ReaderPage? = null
var textPageForMenu: ReaderTextPage? = null
try {
val text = withContext(Dispatchers.IO) {
pageForMenu = pdfDocumentItem.openPage(pdfPageIndex)
textPageForMenu = pageForMenu?.openTextPage()
textPageForMenu?.textPageGetText(
currentRange.first,
currentRange.second - currentRange.first
)
}
if (!text.isNullOrBlank()) {
val combinedRect = Rect(selectedWordScreenRects.first())
selectedWordScreenRects.forEach { combinedRect.union(it) }
if (selectionMethodUsed == PdfSelectionMethod.PDFIUM) {
if (selectionCharRange.value != null && selectedWordScreenRects.isNotEmpty()) {
val currentRange = selectionCharRange.value!!
coroutineScope.launch {
var pageForMenu: ReaderPage? = null
var textPageForMenu: ReaderTextPage? = null
try {
val text = withContext(Dispatchers.IO) {
pageForMenu = pdfDocumentItem.openPage(pdfPageIndex)
textPageForMenu = pageForMenu?.openTextPage()
textPageForMenu?.textPageGetText(
currentRange.first,
currentRange.second - currentRange.first
customMenuState = CustomPdfMenuState(
selectedText = text,
anchorRect = combinedRect,
charRange = currentRange
)
Timber.d(
"Menu shown after drag. Anchor: ${customMenuState?.anchorRect}"
)
} else {
customMenuState = null
}
} catch (e: Exception) {
Timber.e(
e, "Error fetching text for menu after drag"
)
customMenuState = null
} finally {
withContext(NonCancellable) {
withContext(Dispatchers.IO) {
try {
textPageForMenu?.close()
} catch (_: Exception) {
}
try {
pageForMenu?.close()
} catch (_: Exception) {
}
}
}
}
if (!text.isNullOrBlank()) {
val combinedRect = Rect(selectedWordScreenRects.first())
selectedWordScreenRects.forEach { combinedRect.union(it) }
} else {
customMenuState = null
}
} else {
if (ocrSelectionSymbolIndices != null && selectedWordScreenRects.isNotEmpty()) {
val indices = ocrSelectionSymbolIndices!!
val selectedSymbolInfos = allOcrSymbolsForSelection.subList(
indices.first, indices.second
)
if (selectedSymbolInfos.isNotEmpty()) {
val selectedText = buildString {
selectedSymbolInfos.forEachIndexed { index, info ->
append(info.symbol.text)
if (index < selectedSymbolInfos.size - 1) {
val nextInfo = selectedSymbolInfos[index + 1]
if (info.parentLine !== nextInfo.parentLine) {
append('\n')
} else if (info.parentElement !== nextInfo.parentElement) {
append(' ')
}
}
}
}
val firstRect = selectedSymbolInfos.first().symbol.boundingBox!!
val combinedRect = Rect(firstRect)
selectedSymbolInfos.forEach { info -> info.symbol.boundingBox?.let { combinedRect.union(it) } }
customMenuState = CustomPdfMenuState(
selectedText = text,
selectedText = selectedText,
anchorRect = combinedRect,
charRange = currentRange
charRange = Pair(indices.first, indices.second)
)
Timber.d(
"Menu shown after drag. Anchor: ${customMenuState?.anchorRect}"
"Menu shown after OCR drag. Anchor: ${customMenuState?.anchorRect}"
)
} else {
customMenuState = null
}
} catch (e: Exception) {
Timber.e(
e, "Error fetching text for menu after drag"
)
} else {
customMenuState = null
} finally {
withContext(NonCancellable) {
withContext(Dispatchers.IO) {
try {
textPageForMenu?.close()
} catch (_: Exception) {
}
try {
pageForMenu?.close()
} catch (_: Exception) {
}
}
}
}
}
} else {
customMenuState = null
}
} else {
if (ocrSelectionSymbolIndices != null && selectedWordScreenRects.isNotEmpty()) {
val indices = ocrSelectionSymbolIndices!!
val selectedSymbolInfos = allOcrSymbolsForSelection.subList(
indices.first, indices.second
activeDraggingHandle = null
showMagnifier = false
Timber.d(
"PointerInput: Drag on handle completed/cancelled. Menu state: $customMenuState"
)
if (selectedSymbolInfos.isNotEmpty()) {
val selectedText = buildString {
selectedSymbolInfos.forEachIndexed { index, info ->
append(info.symbol.text)
if (index < selectedSymbolInfos.size - 1) {
val nextInfo = selectedSymbolInfos[index + 1]
if (info.parentLine !== nextInfo.parentLine) {
append('\n')
} else if (info.parentElement !== nextInfo.parentElement) {
append(' ')
}
}
}
}
val firstRect = selectedSymbolInfos.first().symbol.boundingBox!!
val combinedRect = Rect(firstRect)
selectedSymbolInfos.forEach { info -> info.symbol.boundingBox?.let { combinedRect.union(it) } }
customMenuState = CustomPdfMenuState(
selectedText = selectedText,
anchorRect = combinedRect,
charRange = Pair(indices.first, indices.second)
)
Timber.d(
"Menu shown after OCR drag. Anchor: ${customMenuState?.anchorRect}"
)
} else {
customMenuState = null
}
} else {
customMenuState = null
}
}
activeDraggingHandle = null
showMagnifier = false
Timber.d(
"PointerInput: Drag on handle completed/cancelled. Menu state: $customMenuState"
)
} else {
val longPressTimeout = viewConfiguration.longPressTimeoutMillis
try {
@ -5632,22 +5637,20 @@ private fun PdfPageRenderer(
val topLeftWindow = coords.localToWindow(topLeftLocal)
val bottomRightWindow = coords.localToWindow(bottomRightLocal)
val windowCenterX = (topLeftWindow.x + bottomRightWindow.x) / 2
val gapPx = with(density) { 16.dp.toPx() }
var yInWindow = (topLeftWindow.y - popupContentSize.height - gapPx).toInt()
if (yInWindow < 0) {
yInWindow = (bottomRightWindow.y + gapPx).toInt()
if (yInWindow + popupContentSize.height > windowSize.height) {
yInWindow = windowSize.height - popupContentSize.height - gapPx.toInt()
}
}
val xInWindow = (windowCenterX - popupContentSize.width / 2).toInt()
.coerceIn(0, windowSize.width - popupContentSize.width)
return IntOffset(xInWindow, yInWindow)
val placement = sharedSelectionMenuPlacement(
viewport = SharedSelectionMenuViewport(windowSize.width, windowSize.height),
popup = SharedSelectionMenuSize(popupContentSize.width, popupContentSize.height),
selection = SharedSelectionMenuRect(
left = topLeftWindow.x,
top = topLeftWindow.y,
right = bottomRightWindow.x,
bottom = bottomRightWindow.y
),
marginPx = gapPx,
gapPx = gapPx
)
return IntOffset(placement.x, placement.y)
}
}
}

View file

@ -1,11 +1,13 @@
package com.aryan.reader.pdf
import android.content.Context
import androidx.annotation.StringRes
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.content.edit
import com.aryan.reader.BuildConfig
import com.aryan.reader.R
import com.aryan.reader.ReaderTheme
import com.aryan.reader.ReaderTexture
import com.aryan.reader.epubreader.SystemUiMode
@ -49,33 +51,46 @@ internal const val PDF_LAYOUT_DEBUG_TAG = "PdfLayoutDebug"
private const val PDF_HIDDEN_TOOLS_DEFAULTS_VERSION_KEY = "pdf_hidden_tools_defaults_version"
private const val PDF_HIDDEN_TOOLS_DEFAULTS_VERSION = 2
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"),
VISUAL_OPTIONS("Visual Options", "Overflow Menu"),
TAP_TO_TURN("Tap to Turn Pages", "Overflow Menu"),
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"),
SCREEN_ORIENTATION("Screen Orientation", "Top Bar"),
AUTO_SCROLL("Auto Scroll", "Overflow Menu"),
TTS_SETTINGS("TTS Settings", "Overflow Menu"),
TTS_REPLACEMENTS("TTS Word Replacements", "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")
enum class PdfReaderTool(@StringRes val titleRes: Int, val category: String) {
DICTIONARY(R.string.tool_external_apps, "Top Bar"),
THEME(R.string.tooltip_theme_desc, "Top Bar"),
LOCK_PANNING(R.string.tooltip_lock_pan, "Top Bar"),
VISUAL_OPTIONS(R.string.menu_visual_options, "Overflow Menu"),
TAP_TO_TURN(R.string.menu_tap_to_turn_pages, "Overflow Menu"),
FULL_SCREEN(R.string.tooltip_fullscreen, "Top Bar"),
SLIDER(R.string.tool_navigation_slider, "Bottom Bar"),
TOC(R.string.tool_sidebar, "Bottom Bar"),
SEARCH(R.string.action_search, "Bottom Bar"),
HIGHLIGHT_ALL(R.string.tool_highlight_selectable_text, "Bottom Bar"),
AI_FEATURES(R.string.ai_features_title, "Bottom Bar"),
EDIT_MODE(R.string.tool_edit_mode, "Bottom Bar"),
TTS_CONTROLS(R.string.tool_tts_controls, "Bottom Bar"),
OCR_LANGUAGE(R.string.menu_ocr_language, "Overflow Menu"),
READING_MODE(R.string.tool_reading_mode, "Overflow Menu"),
KEEP_SCREEN_ON(R.string.menu_keep_screen_on, "Overflow Menu"),
SCREEN_ORIENTATION(R.string.menu_screen_orientation, "Top Bar"),
AUTO_SCROLL(R.string.menu_auto_scroll, "Overflow Menu"),
TTS_SETTINGS(R.string.menu_tts_settings, "Overflow Menu"),
TTS_REPLACEMENTS(R.string.menu_tts_word_replacements, "Overflow Menu"),
BOOKMARK(R.string.content_desc_bookmark, "Overflow Menu"),
PAGE_MANAGEMENT(R.string.tool_page_management, "Overflow Menu"),
REFLOW(R.string.tool_text_view_reflow, "Overflow Menu"),
SHARE(R.string.action_share, "Overflow Menu"),
SAVE_COPY(R.string.action_save_copy_to_device, "Overflow Menu"),
PRINT(R.string.action_print, "Overflow Menu")
}
internal fun defaultPdfHiddenTools(): Set<String> {
return setOf(
PdfReaderTool.SCREEN_ORIENTATION.name,
PdfReaderTool.HIGHLIGHT_ALL.name
)
}
internal fun defaultPdfToolOrder(): List<PdfReaderTool> = PdfReaderTool.entries.toList()
internal fun defaultPdfBottomTools(): Set<String> {
return PdfReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
}
val PdfBuiltInThemes = listOf(
@ -99,10 +114,7 @@ internal fun loadPdfHiddenTools(context: Context): Set<String> {
val savedHiddenTools = prefs.getStringSet(PDF_HIDDEN_TOOLS_KEY, emptySet()).orEmpty()
val defaultsVersion = prefs.getInt(PDF_HIDDEN_TOOLS_DEFAULTS_VERSION_KEY, 0)
if (defaultsVersion < PDF_HIDDEN_TOOLS_DEFAULTS_VERSION) {
val migratedHiddenTools = savedHiddenTools + setOf(
PdfReaderTool.SCREEN_ORIENTATION.name,
PdfReaderTool.HIGHLIGHT_ALL.name
)
val migratedHiddenTools = savedHiddenTools + defaultPdfHiddenTools()
prefs.edit {
putStringSet(PDF_HIDDEN_TOOLS_KEY, migratedHiddenTools)
putInt(PDF_HIDDEN_TOOLS_DEFAULTS_VERSION_KEY, PDF_HIDDEN_TOOLS_DEFAULTS_VERSION)
@ -127,7 +139,7 @@ internal fun loadPdfToolOrder(context: Context): List<PdfReaderTool> {
?.filter { it.isNotBlank() }
?.mapNotNull { name -> PdfReaderTool.entries.firstOrNull { it.name == name } }
.orEmpty()
return (savedTools + PdfReaderTool.entries.filterNot { it in savedTools }).distinct()
return (savedTools + defaultPdfToolOrder().filterNot { it in savedTools }).distinct()
}
internal fun savePdfToolOrder(context: Context, toolOrder: List<PdfReaderTool>) {
@ -137,7 +149,7 @@ internal fun savePdfToolOrder(context: Context, toolOrder: List<PdfReaderTool>)
internal fun loadPdfBottomTools(context: Context): Set<String> {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
val defaultBottomTools = PdfReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
val defaultBottomTools = defaultPdfBottomTools()
return prefs.getStringSet(PDF_BOTTOM_TOOLS_KEY, defaultBottomTools) ?: defaultBottomTools
}

View file

@ -3,6 +3,7 @@
package com.aryan.reader.pdf
import androidx.compose.foundation.clickable
import androidx.annotation.StringRes
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -27,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
@ -34,6 +36,7 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ScreenRotation
@ -45,6 +48,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -77,7 +81,8 @@ data class PdfFlatToolItem(
val type: PdfFlatItemType,
val tool: PdfReaderTool? = null,
val section: PdfToolbarSection? = null,
val title: String? = null
val title: String? = null,
@StringRes val titleRes: Int? = null
)
fun sanitizePdfPlaceholders(list: List<PdfFlatToolItem>): List<PdfFlatToolItem> {
@ -92,7 +97,7 @@ fun sanitizePdfPlaceholders(list: List<PdfFlatToolItem>): List<PdfFlatToolItem>
}
PdfToolbarSection.entries.forEach { section ->
result.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, title = section.title))
result.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, titleRes = section.titleRes))
val tools = sectionMap[section] ?: emptyList()
if (tools.isEmpty()) {
result.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
@ -108,6 +113,51 @@ fun sanitizePdfPlaceholders(list: List<PdfFlatToolItem>): List<PdfFlatToolItem>
return result
}
private val pdfReorderableToolbarTools = setOf(
PdfReaderTool.DICTIONARY, PdfReaderTool.THEME, PdfReaderTool.LOCK_PANNING,
PdfReaderTool.SLIDER, PdfReaderTool.TOC, PdfReaderTool.SEARCH,
PdfReaderTool.HIGHLIGHT_ALL, PdfReaderTool.AI_FEATURES,
PdfReaderTool.EDIT_MODE, PdfReaderTool.TTS_CONTROLS,
PdfReaderTool.SCREEN_ORIENTATION
)
internal fun buildPdfToolbarItems(
hiddenTools: Set<String>,
toolOrder: List<PdfReaderTool>,
bottomTools: Set<String>
): List<PdfFlatToolItem> {
val toolbarTools = toolOrder.filter { it in pdfReorderableToolbarTools }
val topTools = toolbarTools.filter { !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
val bottomToolsList = toolbarTools.filter { bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
val hiddenToolsList = toolbarTools.filter { hiddenTools.contains(it.name) }
val moreTools = toolOrder.filter { it !in pdfReorderableToolbarTools }
val list = mutableListOf<PdfFlatToolItem>()
PdfToolbarSection.entries.forEach { section ->
val tools = when (section) {
PdfToolbarSection.TOP -> topTools
PdfToolbarSection.BOTTOM -> bottomToolsList
PdfToolbarSection.HIDDEN -> hiddenToolsList
}
list.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, titleRes = section.titleRes))
if (tools.isEmpty()) {
list.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
} else {
tools.forEach { tool ->
list.add(PdfFlatToolItem("tool_${tool.name}", PdfFlatItemType.TOOL, tool = tool, section = section))
}
}
}
list.add(PdfFlatToolItem("more_header", PdfFlatItemType.MORE_HEADER, titleRes = R.string.toolbar_more_menu))
moreTools.forEach { tool ->
list.add(PdfFlatToolItem("more_${tool.name}", PdfFlatItemType.MORE_TOOL, tool = tool))
}
return list
}
class PdfDragDropState(
val lazyListState: LazyListState,
val onMove: (String, String) -> Unit
@ -142,54 +192,20 @@ fun PdfCustomizeToolsSheet(
onPlacementUpdate: (Set<String>) -> Unit,
onDismiss: () -> Unit
) {
val reorderableToolbarTools = setOf(
PdfReaderTool.DICTIONARY, PdfReaderTool.THEME, PdfReaderTool.LOCK_PANNING,
PdfReaderTool.SLIDER, PdfReaderTool.TOC, PdfReaderTool.SEARCH,
PdfReaderTool.HIGHLIGHT_ALL, PdfReaderTool.AI_FEATURES,
PdfReaderTool.EDIT_MODE, PdfReaderTool.TTS_CONTROLS,
PdfReaderTool.SCREEN_ORIENTATION
)
var localHiddenTools by remember { mutableStateOf(hiddenTools) }
var flatItems by remember {
mutableStateOf<List<PdfFlatToolItem>>(
run {
val toolbarTools = toolOrder.filter { it in reorderableToolbarTools }
val topTools = toolbarTools.filter { !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
val bottomToolsList = toolbarTools.filter { bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
val hiddenToolsList = toolbarTools.filter { hiddenTools.contains(it.name) }
val moreTools = toolOrder.filter { it !in reorderableToolbarTools }
val list = mutableListOf<PdfFlatToolItem>()
PdfToolbarSection.entries.forEach { section ->
val tools = when(section) {
PdfToolbarSection.TOP -> topTools
PdfToolbarSection.BOTTOM -> bottomToolsList
PdfToolbarSection.HIDDEN -> hiddenToolsList
}
list.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, title = section.title))
if (tools.isEmpty()) {
list.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
} else {
tools.forEach { tool ->
list.add(PdfFlatToolItem("tool_${tool.name}", PdfFlatItemType.TOOL, tool = tool, section = section))
}
}
}
list.add(PdfFlatToolItem("more_header", PdfFlatItemType.MORE_HEADER, title = "More menu"))
moreTools.forEach { tool ->
list.add(PdfFlatToolItem("more_${tool.name}", PdfFlatItemType.MORE_TOOL, tool = tool))
}
list
}
buildPdfToolbarItems(
hiddenTools = hiddenTools,
toolOrder = toolOrder,
bottomTools = bottomTools
)
)
}
val commitDragDrop = {
val newHidden = localHiddenTools.filter { toolName ->
toolOrder.find { it.name == toolName } !in reorderableToolbarTools
toolOrder.find { it.name == toolName } !in pdfReorderableToolbarTools
}.toMutableSet()
val newBottom = mutableSetOf<String>()
@ -247,6 +263,22 @@ fun PdfCustomizeToolsSheet(
}
}
val resetToDefault = {
val defaultHiddenTools = defaultPdfHiddenTools()
val defaultToolOrder = defaultPdfToolOrder()
val defaultBottomTools = defaultPdfBottomTools()
localHiddenTools = defaultHiddenTools
flatItems = buildPdfToolbarItems(
hiddenTools = defaultHiddenTools,
toolOrder = defaultToolOrder,
bottomTools = defaultBottomTools
)
onUpdate(defaultHiddenTools)
onPlacementUpdate(defaultBottomTools)
onOrderUpdate(defaultToolOrder)
}
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
@ -269,6 +301,11 @@ fun PdfCustomizeToolsSheet(
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
TextButton(onClick = resetToDefault) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.action_reset))
}
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close))
}
@ -300,8 +337,9 @@ fun PdfCustomizeToolsSheet(
) {
when (item.type) {
PdfFlatItemType.SECTION_HEADER -> {
val titleRes = item.titleRes
Text(
text = item.title ?: "",
text = if (titleRes != null) stringResource(titleRes) else item.title.orEmpty(),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
@ -317,7 +355,7 @@ fun PdfCustomizeToolsSheet(
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
Text("Drop tools here", color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(stringResource(R.string.toolbar_drop_tools_here), color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
PdfFlatItemType.TOOL -> {
@ -334,8 +372,9 @@ fun PdfCustomizeToolsSheet(
)
}
PdfFlatItemType.MORE_HEADER -> {
val titleRes = item.titleRes
Text(
text = item.title ?: "More menu",
text = if (titleRes != null) stringResource(titleRes) else item.title ?: stringResource(R.string.toolbar_more_menu),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 4.dp)
@ -343,7 +382,7 @@ fun PdfCustomizeToolsSheet(
}
PdfFlatItemType.MORE_TOOL -> {
PdfMoreToolVisibilityRow(
title = item.tool!!.title,
title = stringResource(item.tool!!.titleRes),
visible = !localHiddenTools.contains(item.tool.name),
onToggle = {
localHiddenTools = if (localHiddenTools.contains(item.tool.name)) {
@ -386,18 +425,19 @@ private fun PdfToolbarDragRow(
PdfToolPreviewIcon(tool)
Spacer(Modifier.width(16.dp))
Text(
text = tool.title,
text = stringResource(tool.titleRes),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Icon(
Icons.Default.Menu,
contentDescription = "Drag to reorder",
contentDescription = stringResource(R.string.content_desc_drag_to_reorder),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.size(48.dp)
.padding(12.dp)
.size(32.dp)
.padding(6.dp)
.clip(CircleShape)
.pointerInput(tool) {
detectDragGestures(
onDragStart = { onDragStart() },
@ -453,7 +493,7 @@ private fun PdfToolbarDragRow(
PdfToolPreviewIcon(tool)
Spacer(Modifier.width(12.dp))
Text(
text = tool.title,
text = stringResource(tool.titleRes),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
@ -489,27 +529,28 @@ private fun PdfMoreToolVisibilityRow(
}
}
enum class PdfToolbarSection(val title: String) {
TOP("Top Bar"),
BOTTOM("Bottom Bar"),
HIDDEN("Hidden Tools")
enum class PdfToolbarSection(@StringRes val titleRes: Int) {
TOP(R.string.toolbar_top_bar),
BOTTOM(R.string.toolbar_bottom_bar),
HIDDEN(R.string.toolbar_hidden_tools)
}
@Composable
private fun PdfToolPreviewIcon(tool: PdfReaderTool) {
val title = stringResource(tool.titleRes)
when (tool) {
PdfReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.LOCK_PANNING -> Icon(Icons.Default.LockOpen, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.HIGHLIGHT_ALL -> Icon(painterResource(id = R.drawable.highlight_text), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.EDIT_MODE -> Icon(Icons.Default.Edit, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.SCREEN_ORIENTATION -> Icon(Icons.Default.ScreenRotation, contentDescription = tool.title, modifier = Modifier.size(20.dp))
else -> Icon(Icons.Default.MoreVert, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.LOCK_PANNING -> Icon(Icons.Default.LockOpen, contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.HIGHLIGHT_ALL -> Icon(painterResource(id = R.drawable.highlight_text), contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.EDIT_MODE -> Icon(Icons.Default.Edit, contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = title, modifier = Modifier.size(20.dp))
PdfReaderTool.SCREEN_ORIENTATION -> Icon(Icons.Default.ScreenRotation, contentDescription = title, modifier = Modifier.size(20.dp))
else -> Icon(Icons.Default.MoreVert, contentDescription = title, modifier = Modifier.size(20.dp))
}
}
@ -556,7 +597,7 @@ fun PdfVisualOptionsSheet(
options = SystemUiMode.entries,
selectedOption = systemUiMode,
onOptionSelected = onSystemUiModeChange,
getLabel = { it.title }
getLabel = { stringResource(it.titleRes) }
)
Spacer(modifier = Modifier.height(20.dp))

View file

@ -330,7 +330,7 @@ internal fun PdfTopBar(
if (hiddenToolbarTools.isNotEmpty()) {
DropdownMenuItem(
text = { Text("Hidden tools") },
text = { Text(stringResource(R.string.toolbar_hidden_tools_menu)) },
onClick = { showHiddenToolsExpanded = !showHiddenToolsExpanded },
trailingIcon = {
Icon(
@ -665,7 +665,7 @@ private fun HiddenPdfToolMenuItem(
else -> true
}
DropdownMenuItem(
text = { Text(tool.title) },
text = { Text(stringResource(tool.titleRes)) },
enabled = enabled,
onClick = {
closeMenu()

View file

@ -2118,7 +2118,7 @@ fun PdfViewerScreen(
words.take(6).joinToString(" ") + "..."
} else {
Timber.d("No words found. Falling back to 'Page X' title.")
"Page ${pageIndex + 1}"
context.getString(R.string.pdf_page_short, pageIndex + 1)
}
val chapterTitle =
@ -2472,7 +2472,7 @@ fun PdfViewerScreen(
}
if (virtualPage is VirtualPage.BlankPage) {
onUpdate(SummarizationResult(error = "Cannot summarize a blank page."))
onUpdate(SummarizationResult(error = context.getString(R.string.pdf_error_blank_page_summary)))
onFinish()
return
}
@ -2480,7 +2480,7 @@ fun PdfViewerScreen(
val pdfPageIndex = (virtualPage as? VirtualPage.PdfPage)?.pdfIndex ?: currentPageIndex
val doc = pdfDocument ?: run {
onUpdate(SummarizationResult(error = "Document not loaded."))
onUpdate(SummarizationResult(error = context.getString(R.string.pdf_error_document_not_loaded)))
onFinish()
return
}
@ -2593,7 +2593,7 @@ fun PdfViewerScreen(
if (fullText.isEmpty() && lastResult?.error == null) {
onUpdate(
SummarizationResult(
error = "Failed to parse summary from server response."
error = context.getString(R.string.ai_error_parse_summary)
)
)
}
@ -2607,17 +2607,21 @@ fun PdfViewerScreen(
val errorDetail = try {
errorBody?.let { JSONObject(it).getString("detail") }
} catch (_: Exception) {
"Could not fetch summary."
context.getString(R.string.ai_error_fetch_summary)
}
onUpdate(
SummarizationResult(
error = "Error: $responseCode. ${errorDetail ?: "An unknown server error occurred."}"
error = context.getString(
R.string.ai_error_with_code,
responseCode,
errorDetail ?: context.getString(R.string.error_unknown_server)
)
)
)
}
} catch (e: Exception) {
Timber.e(e, "Exception during PDF page summarization: ${e.message}")
onUpdate(SummarizationResult(error = "An error occurred: ${e.localizedMessage}"))
onUpdate(SummarizationResult(error = context.getString(R.string.error_occurred_format, e.localizedMessage)))
} finally {
pageBitmap?.recycle()
connection?.disconnect()
@ -2783,8 +2787,8 @@ fun PdfViewerScreen(
val chunks = splitTextIntoChunks(textToChunk)
val bookTitle = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument?.getDocumentMeta()?.title?.takeIf { it.isNotBlank() }
?: effectivePdfUri.lastPathSegment ?: "Document"
val pageTitle = "Page ${pageToRead + 1}"
?: effectivePdfUri.lastPathSegment ?: context.getString(R.string.default_document_title)
val pageTitle = context.getString(R.string.pdf_page_short, pageToRead + 1)
val ttsChunks = chunks.mapIndexed { index, text -> TtsChunk(text, "", index) }
@ -2805,8 +2809,8 @@ fun PdfViewerScreen(
}
} else {
val finalError = when {
ocrAttempted -> "OCR found no text on this page."
else -> "Page seems empty or text not extractable."
ocrAttempted -> context.getString(R.string.error_no_text_on_page_after_ocr)
else -> context.getString(R.string.error_page_text_not_extractable)
}
val nextPage = pageToRead + 1
@ -3216,7 +3220,7 @@ fun PdfViewerScreen(
}
} else {
Timber.e(e, "Error loading fixed-layout document")
errorMessage = "Error loading document: ${e.localizedMessage}"
errorMessage = context.getString(R.string.error_loading_document_format, e.localizedMessage)
isLoadingDocument = false
}
if (pdfDocument == null) {
@ -3516,7 +3520,7 @@ fun PdfViewerScreen(
results.add(
SearchResult(
locationInSource = match.pageIndex,
locationTitle = "Page ${match.pageIndex + 1}",
locationTitle = context.getString(R.string.pdf_page_short, match.pageIndex + 1),
snippet = parseSnippet(match.snippet),
query = query,
occurrenceIndexInLocation = occurrenceIndex,
@ -3529,7 +3533,7 @@ fun PdfViewerScreen(
results.add(
SearchResult(
locationInSource = match.pageIndex,
locationTitle = "Page ${match.pageIndex + 1}",
locationTitle = context.getString(R.string.pdf_page_short, match.pageIndex + 1),
snippet = parseSnippet(match.snippet),
query = query,
occurrenceIndexInLocation = 0,
@ -3678,6 +3682,9 @@ fun PdfViewerScreen(
userHighlights = visibleUserHighlights,
currentPage = currentPage,
totalPages = totalDisplayPages,
isTabsEnabled = isPdfTabStripVisible,
openTabs = openTabs,
activeTabBookId = activeTabBookId,
customHighlightColors = customHighlightColors,
onPageSelected = { targetPage ->
coroutineScope.launch {
@ -3694,6 +3701,29 @@ fun PdfViewerScreen(
}
}
},
onTabSelected = { tabBookId ->
coroutineScope.launch {
currentBookId?.let { tabStateMap[it] = currentPage }
saveAllData(true).join()
viewModel.switchTab(tabBookId)
}
},
onTabClosed = { tabBookId ->
coroutineScope.launch {
val isSelected = tabBookId == activeTabBookId
if (isSelected) saveAllData(true).join()
viewModel.closeTab(tabBookId)
if (isSelected && openTabs.size == 1) {
onNavigateBack()
}
}
},
onNewTabClick = {
coroutineScope.launch {
drawerState.close()
showNewTabSheet = true
}
},
onRenameBookmark = { bookmarkToRename, newTitle ->
if (newTitle.isNotBlank()) {
val updatedBookmark = bookmarkToRename.copy(title = newTitle)
@ -6376,7 +6406,7 @@ fun PdfViewerScreen(
AiHubBottomSheet(
bookTitle = bookTitle,
currentChapterIndex = currentPageForDisplay,
chapterTitle = "Page ${currentPageForDisplay + 1}",
chapterTitle = stringResource(R.string.pdf_page_short, currentPageForDisplay + 1),
summaryCacheManager = summaryCacheManager,
summarizationResult = summarizationResult,
isSummarizationLoading = isSummarizationLoading,
@ -6412,7 +6442,12 @@ fun PdfViewerScreen(
isSummarizationLoading = false
val finalSummary = summarizationResult?.summary
if (!finalSummary.isNullOrBlank() && summarizationResult?.error == null) {
summaryCacheManager.saveSummary(bookTitle, currentPageForDisplay, "Page ${currentPageForDisplay + 1}", finalSummary)
summaryCacheManager.saveSummary(
bookTitle,
currentPageForDisplay,
context.getString(R.string.pdf_page_short, currentPageForDisplay + 1),
finalSummary
)
}
}
)
@ -6959,7 +6994,7 @@ fun PdfViewerScreen(
AlertDialog(
onDismissRequest = { clickedLinkUrl = null },
title = { Text(stringResource(R.string.dialog_external_link_title)) },
text = { Text(stringResource(R.string.desc_external_link_warning)) },
text = { Text(stringResource(R.string.desc_external_link_warning, url)) },
confirmButton = {
TextButton(
onClick = {