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:
parent
759d4b73a0
commit
056485a140
77 changed files with 9184 additions and 5947 deletions
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue