Support notes with highlights (#150)
* Added support for highlight notes in EPUB reader * fix(epub annots): ensure persistent highlights and preserve notes on color change - refactor: unified processAndAddHighlight across scroll and paginated modes - fix: re-apply highlights to virtualized DOM elements on chunk load - fix: replaced '|' with ';;' in JS bridge to prevent CFI parsing errors * feat(epub): redesign annotation UX with theme-aware Bottom Sheet * Updated highlight handling to use full CFI strings instead of splitting into parts during deletion and style updates. Adjusted `processAndAddHighlight` to return the final CFI and updated the delete button UI to use a contained button style. * Added support for notes in PDF highlights. * Implemented support for custom highlight colors in the PDF reader. * Improved PDF annotation notes and theme handling in the bottom sheet.
This commit is contained in:
parent
c8f361376f
commit
65e0570d0e
12 changed files with 2026 additions and 744 deletions
|
|
@ -24,6 +24,7 @@ import android.graphics.Rect
|
|||
import android.graphics.RectF
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -31,10 +32,12 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
|
@ -45,23 +48,40 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.CopyAll
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
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
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
|
|
@ -72,7 +92,6 @@ import com.aryan.reader.pdf.ocr.OcrElement
|
|||
import com.aryan.reader.pdf.ocr.OcrLine
|
||||
import com.aryan.reader.pdf.ocr.OcrResult
|
||||
import com.aryan.reader.pdf.ocr.OcrSymbol
|
||||
import io.legere.pdfiumandroid.suspend.PdfTextPageKt
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
|
|
@ -111,7 +130,8 @@ data class PdfUserHighlight(
|
|||
val bounds: List<RectF>,
|
||||
val color: PdfHighlightColor,
|
||||
val text: String,
|
||||
val range: Pair<Int, Int>
|
||||
val range: Pair<Int, Int>,
|
||||
val note: String? = null
|
||||
)
|
||||
|
||||
internal data class CustomPdfMenuState(
|
||||
|
|
@ -122,7 +142,9 @@ internal data class CustomPdfMenuState(
|
|||
val highlightId: String? = null,
|
||||
val isComment: Boolean = false,
|
||||
val author: String? = null,
|
||||
val annotation: EmbeddedAnnotation? = null
|
||||
val annotation: EmbeddedAnnotation? = null,
|
||||
val note: String? = null,
|
||||
val selectedColor: PdfHighlightColor? = null
|
||||
)
|
||||
|
||||
internal enum class PdfSelectionMethod {
|
||||
|
|
@ -197,6 +219,8 @@ private fun CommentThread(replies: List<EmbeddedAnnotation>, depth: Int) {
|
|||
internal fun PdfSelectionMenuPopup(
|
||||
menuState: CustomPdfMenuState,
|
||||
popupPositionProvider: PopupPositionProvider,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit,
|
||||
onCopy: (String) -> Unit,
|
||||
onAiDefine: (String) -> Unit,
|
||||
|
|
@ -205,7 +229,8 @@ internal fun PdfSelectionMenuPopup(
|
|||
onSelectAll: () -> Unit,
|
||||
onColorSelected: (PdfHighlightColor) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onTts: (() -> Unit)? = null
|
||||
onTts: (() -> Unit)? = null,
|
||||
onNote: (() -> Unit)? = null
|
||||
) {
|
||||
Popup(
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
|
|
@ -224,6 +249,48 @@ internal fun PdfSelectionMenuPopup(
|
|||
modifier = Modifier.widthIn(max = 300.dp)
|
||||
) {
|
||||
Column(modifier = if (menuState.isComment) Modifier.fillMaxWidth() else Modifier.width(IntrinsicSize.Max)) {
|
||||
if (!menuState.note.isNullOrBlank()) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 140.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Note",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
"Note",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = menuState.note,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (menuState.isComment) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -264,15 +331,30 @@ internal fun PdfSelectionMenuPopup(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PdfHighlightColor.entries.forEach { colorEnum ->
|
||||
val displayColor = customHighlightColors[colorEnum] ?: colorEnum.color
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 6.dp).size(32.dp)
|
||||
.background(colorEnum.color, CircleShape).clip(CircleShape)
|
||||
.background(displayColor, CircleShape).clip(CircleShape)
|
||||
.clickable {
|
||||
Timber.tag("PdfHighlightDebug")
|
||||
.d("Color box clicked: $colorEnum")
|
||||
onColorSelected(colorEnum)
|
||||
})
|
||||
}
|
||||
if (onPaletteClick != null) {
|
||||
val rainbowColors = listOf(
|
||||
Color.Red, Color.Magenta, Color.Blue, Color.Cyan, Color.Green, Color.Yellow, Color.Red
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Brush.sweepGradient(rainbowColors))
|
||||
.clickable { onPaletteClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
|
@ -288,6 +370,11 @@ internal fun PdfSelectionMenuPopup(
|
|||
actions.add(MenuActionItem(imageVector = Icons.Default.Search, label = "Search", onClick = { onSearch(menuState.selectedText) }))
|
||||
}
|
||||
|
||||
if (onNote != null) {
|
||||
val noteLabel = if (menuState.note.isNullOrBlank()) "Note" else "Edit"
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
|
||||
}
|
||||
|
||||
if (!menuState.isExistingHighlight) {
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = { onSelectAll() }))
|
||||
}
|
||||
|
|
@ -536,4 +623,214 @@ internal fun mergePdfRectsIntoLines(rects: List<RectF>): List<RectF> {
|
|||
return merged.map { m ->
|
||||
RectF(m[0], m[3], m[2], m[1])
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PdfHighlightColorRow(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedColor: PdfHighlightColor? = null,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onColorSelect: (PdfHighlightColor) -> Unit,
|
||||
onPaletteClick: (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PdfHighlightColor.entries.forEach { colorEnum ->
|
||||
val displayColor = customHighlightColors[colorEnum] ?: colorEnum.color
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(displayColor)
|
||||
.clickable { onColorSelect(colorEnum) }
|
||||
.border(
|
||||
width = if (selectedColor == colorEnum) 3.dp else 1.dp,
|
||||
color = if (selectedColor == colorEnum) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
if (selectedColor == colorEnum) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = if (displayColor.luminance() > 0.5f) Color.Black else Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onPaletteClick != null) {
|
||||
val rainbowColors = listOf(
|
||||
Color.Red, Color.Magenta, Color.Blue, Color.Cyan, Color.Green, Color.Yellow, Color.Red
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Brush.sweepGradient(rainbowColors))
|
||||
.clickable { onPaletteClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PdfAnnotationBottomSheet(
|
||||
highlight: PdfUserHighlight,
|
||||
effectiveBg: Color,
|
||||
effectiveText: Color,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: (() -> Unit)? = null,
|
||||
onColorChange: (PdfHighlightColor) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var noteText by remember { mutableStateOf(highlight.note ?: "") }
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = effectiveBg,
|
||||
contentColor = effectiveText,
|
||||
contentWindowInsets = { WindowInsets.navigationBars }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
val displayColor = customHighlightColors[highlight.color] ?: highlight.color.color
|
||||
|
||||
PdfHighlightColorRow(
|
||||
selectedColor = highlight.color,
|
||||
customHighlightColors = customHighlightColors,
|
||||
onColorSelect = onColorChange,
|
||||
onPaletteClick = onPaletteClick,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
Surface(
|
||||
color = displayColor.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(1.dp, displayColor.copy(alpha = 0.3f)),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
Box(modifier = Modifier.width(6.dp).fillMaxHeight().background(displayColor))
|
||||
Text(
|
||||
text = "\"${highlight.text}\"",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontStyle = FontStyle.Italic),
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = effectiveText.copy(alpha = 0.9f),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
PdfBottomSheetToolButton(icon = R.drawable.copy, label = "Copy", effectiveText = effectiveText, onClick = onCopy)
|
||||
PdfBottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", effectiveText = effectiveText, onClick = onDictionary)
|
||||
PdfBottomSheetToolButton(icon = R.drawable.translate, label = "Translate", effectiveText = effectiveText, onClick = onTranslate)
|
||||
PdfBottomSheetToolButton(icon = R.drawable.search, label = "Search", effectiveText = effectiveText, onClick = onSearch)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = noteText,
|
||||
onValueChange = { noteText = it },
|
||||
placeholder = { Text("Add a note...", color = effectiveText.copy(alpha = 0.5f)) },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp),
|
||||
maxLines = 5,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = effectiveText.copy(alpha = 0.3f),
|
||||
focusedTextColor = effectiveText,
|
||||
unfocusedTextColor = effectiveText
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onDelete,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Delete")
|
||||
}
|
||||
Button(
|
||||
onClick = { onSave(noteText) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
) {
|
||||
Text("Save Note")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfBottomSheetToolButton(
|
||||
icon: Int,
|
||||
label: String,
|
||||
effectiveText: Color,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).clickable(onClick = onClick).padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = label,
|
||||
tint = effectiveText.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = effectiveText.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +368,7 @@ data class PageSelectionData(
|
|||
val selectionHighlightColor: Color,
|
||||
val pageIndex: Int,
|
||||
val userHighlightScreenRects: StableHolder<List<Pair<PdfUserHighlight, List<Rect>>>>,
|
||||
val customHighlightColors: StableHolder<Map<PdfHighlightColor, Color>>
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
@ -437,8 +438,11 @@ internal fun PdfPageComposable(
|
|||
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit = { _,_,_,_ -> },
|
||||
onHighlightUpdate: (String, PdfHighlightColor) -> Unit = { _,_ -> },
|
||||
onHighlightDelete: (String) -> Unit = {},
|
||||
onNoteRequested: (String?) -> Unit = {},
|
||||
onTts: (Int, Int) -> Unit = { _, _ -> },
|
||||
activeToolThickness: Float = 0f
|
||||
activeToolThickness: Float = 0f,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: (() -> Unit)? = null
|
||||
) {
|
||||
val pdfDocumentItem = pdfDocument.item
|
||||
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
||||
|
|
@ -2552,16 +2556,7 @@ internal fun PdfPageComposable(
|
|||
|
||||
if (hitHighlightPair != null && tappedRect != null) {
|
||||
val hitHighlight = hitHighlightPair.first
|
||||
val combinedRect = Rect(hitHighlightPair.second.first())
|
||||
hitHighlightPair.second.forEach { combinedRect.union(it) }
|
||||
|
||||
customMenuState = CustomPdfMenuState(
|
||||
selectedText = hitHighlight.text,
|
||||
anchorRect = combinedRect,
|
||||
charRange = hitHighlight.range,
|
||||
isExistingHighlight = true,
|
||||
highlightId = hitHighlight.id
|
||||
)
|
||||
onNoteRequested(hitHighlight.id)
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
@ -3568,7 +3563,8 @@ internal fun PdfPageComposable(
|
|||
searchHighlightMode,
|
||||
searchFocusedColor,
|
||||
searchAllColor,
|
||||
userHighlightScreenRects
|
||||
userHighlightScreenRects,
|
||||
customHighlightColors
|
||||
) {
|
||||
PageSelectionData(
|
||||
pageLinks = StableHolder(pageLinks),
|
||||
|
|
@ -3593,6 +3589,7 @@ internal fun PdfPageComposable(
|
|||
mergedSearchAllRects = StableHolder(mergedSearchAllRects),
|
||||
searchHighlightMode = searchHighlightMode,
|
||||
userHighlightScreenRects = StableHolder(userHighlightScreenRects),
|
||||
customHighlightColors = StableHolder(customHighlightColors)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3622,6 +3619,7 @@ internal fun PdfPageComposable(
|
|||
onHighlightUpdate = onHighlightUpdate,
|
||||
onHighlightDelete = onHighlightDelete,
|
||||
onTts = onTts,
|
||||
onNote = onNoteRequested,
|
||||
teardropHeightPx = teardropHeightPxState.value,
|
||||
activeDraggingHandle = activeDraggingHandle,
|
||||
showMagnifier = showMagnifier,
|
||||
|
|
@ -3833,7 +3831,9 @@ internal fun PdfPageComposable(
|
|||
onTextBoxDrag = onTextBoxDrag,
|
||||
onTextBoxDragEnd = onTextBoxDragEnd,
|
||||
onDragPageTurn = onDragPageTurn,
|
||||
draggingBoxId = draggingBoxId
|
||||
draggingBoxId = draggingBoxId,
|
||||
customHighlightColors = customHighlightColors,
|
||||
onPaletteClick = onPaletteClick
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3955,7 +3955,8 @@ private fun PdfHighlightsLayer(
|
|||
scrimColorForTextHighlight: Color,
|
||||
allTextPageHighlightColor: Color,
|
||||
ttsHighlightColor: Color,
|
||||
selectionHighlightColor: Color
|
||||
selectionHighlightColor: Color,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap()
|
||||
) {
|
||||
Timber.d("PdfHighlightsLayer Recompose")
|
||||
Canvas(modifier = Modifier
|
||||
|
|
@ -4098,10 +4099,11 @@ private fun PdfHighlightsLayer(
|
|||
|
||||
// 9. Persistent User Highlights
|
||||
userHighlightScreenRects.forEach { (highlight, screenRects) ->
|
||||
val displayColor = customHighlightColors[highlight.color] ?: highlight.color.color
|
||||
screenRects.forEach { r ->
|
||||
if (isVisible(r)) {
|
||||
drawRect(
|
||||
color = highlight.color.color.copy(alpha = 0.4f),
|
||||
color = displayColor.copy(alpha = 0.4f),
|
||||
topLeft = Offset(r.left.toFloat(), r.top.toFloat()),
|
||||
size = Size(r.width().toFloat(), r.height().toFloat())
|
||||
)
|
||||
|
|
@ -4471,7 +4473,8 @@ private fun PdfPageSelectionsLayer(
|
|||
scrimColorForTextHighlight: Color,
|
||||
allTextPageHighlightColor: Color,
|
||||
ttsHighlightColor: Color,
|
||||
selectionHighlightColor: Color
|
||||
selectionHighlightColor: Color,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap()
|
||||
) {
|
||||
SideEffect {
|
||||
Timber.tag("PdfDrawPerf").v("SELECTIONS LAYER: Recomposing")
|
||||
|
|
@ -4498,7 +4501,8 @@ private fun PdfPageSelectionsLayer(
|
|||
scrimColorForTextHighlight = scrimColorForTextHighlight,
|
||||
allTextPageHighlightColor = allTextPageHighlightColor,
|
||||
ttsHighlightColor = ttsHighlightColor,
|
||||
selectionHighlightColor = selectionHighlightColor
|
||||
selectionHighlightColor = selectionHighlightColor,
|
||||
customHighlightColors = customHighlightColors
|
||||
)
|
||||
|
||||
val highlightTime = (System.nanoTime() - highlightStart) / 1_000_000f
|
||||
|
|
@ -4561,12 +4565,14 @@ private fun PdfPageRenderer(
|
|||
onTextBoxDragEnd: () -> Unit,
|
||||
onDragPageTurn: (Int) -> Unit,
|
||||
draggingBoxId: String? = null,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: (() -> Unit)? = null,
|
||||
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit,
|
||||
onHighlightUpdate: (String, PdfHighlightColor) -> Unit,
|
||||
onHighlightDelete: (String) -> Unit,
|
||||
onTts: (Int, Int) -> Unit,
|
||||
activeToolThickness: Float
|
||||
|
||||
activeToolThickness: Float,
|
||||
onNote: (String?) -> Unit,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
|
|
@ -4604,7 +4610,8 @@ private fun PdfPageRenderer(
|
|||
scrimColorForTextHighlight = selectionData.scrimColorForTextHighlight,
|
||||
allTextPageHighlightColor = selectionData.allTextPageHighlightColor,
|
||||
ttsHighlightColor = selectionData.ttsHighlightColor,
|
||||
selectionHighlightColor = selectionData.selectionHighlightColor
|
||||
selectionHighlightColor = selectionData.selectionHighlightColor,
|
||||
customHighlightColors = selectionData.customHighlightColors.item
|
||||
)
|
||||
|
||||
// Layer 3: Annotations & Text
|
||||
|
|
@ -4950,7 +4957,21 @@ private fun PdfPageRenderer(
|
|||
onTts = {
|
||||
onTts(selectionData.pageIndex, menuState.charRange.first)
|
||||
onMenuDismiss()
|
||||
}
|
||||
},
|
||||
onNote = {
|
||||
if (menuState.isExistingHighlight && menuState.highlightId != null) {
|
||||
onNote(menuState.highlightId)
|
||||
} else {
|
||||
onNote(null)
|
||||
onHighlightAdd(
|
||||
selectionData.pageIndex, menuState.charRange, menuState.selectedText,
|
||||
PdfHighlightColor.YELLOW
|
||||
)
|
||||
}
|
||||
onMenuDismiss()
|
||||
},
|
||||
customHighlightColors = selectionData.customHighlightColors.item,
|
||||
onPaletteClick = onPaletteClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,8 +230,11 @@ internal fun PdfVerticalReader(
|
|||
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit = { _,_,_,_ -> },
|
||||
onHighlightUpdate: (String, PdfHighlightColor) -> Unit = { _,_ -> },
|
||||
onHighlightDelete: (String) -> Unit = {},
|
||||
onNoteRequested: (String?) -> Unit = {},
|
||||
onTts: (Int, Int) -> Unit = { _, _ -> },
|
||||
activeToolThickness: Float = 0f
|
||||
activeToolThickness: Float = 0f,
|
||||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: () -> Unit = {}
|
||||
) {
|
||||
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
|
||||
DisposableEffect(state) {
|
||||
|
|
@ -1576,8 +1579,11 @@ internal fun PdfVerticalReader(
|
|||
onHighlightAdd = onHighlightAdd,
|
||||
onHighlightUpdate = onHighlightUpdate,
|
||||
onHighlightDelete = onHighlightDelete,
|
||||
onNoteRequested = onNoteRequested,
|
||||
onTts = onTts,
|
||||
activeToolThickness = activeToolThickness,
|
||||
customHighlightColors = customHighlightColors,
|
||||
onPaletteClick = onPaletteClick,
|
||||
onTextBoxDragStart = { box, localTopLeft, touchOffset ->
|
||||
val currentZoom = zoomAnimatable.value
|
||||
val panX = panXAnimatable.value
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ package com.aryan.reader.pdf
|
|||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
|
|
@ -43,6 +44,7 @@ import android.print.PrintDocumentInfo
|
|||
import android.print.PrintManager
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Base64
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
|
|
@ -119,7 +121,6 @@ import androidx.compose.material.icons.filled.ArrowUpward
|
|||
import androidx.compose.material.icons.filled.Brush
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Fullscreen
|
||||
import androidx.compose.material.icons.filled.FullscreenExit
|
||||
|
|
@ -144,6 +145,7 @@ import androidx.compose.material3.DrawerValue
|
|||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
|
@ -153,6 +155,7 @@ import androidx.compose.material3.LinearProgressIndicator
|
|||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
|
|
@ -167,6 +170,7 @@ import androidx.compose.material3.TabRow
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -262,6 +266,7 @@ import com.aryan.reader.AiDefinitionResult
|
|||
import com.aryan.reader.BuildConfig
|
||||
import com.aryan.reader.DeviceVoiceSettingsSheet
|
||||
import com.aryan.reader.FileType
|
||||
import com.aryan.reader.HighlightColorPickerDialog
|
||||
import com.aryan.reader.MainViewModel
|
||||
import com.aryan.reader.R
|
||||
import com.aryan.reader.ReaderTheme
|
||||
|
|
@ -360,6 +365,24 @@ private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
|
|||
private const val PDF_THEME_KEY = "pdf_reader_theme"
|
||||
private const val PDF_KEEP_SCREEN_ON_KEY = "pdf_keep_screen_on_enabled"
|
||||
|
||||
private fun loadCustomHighlightColors(context: Context): Map<PdfHighlightColor, Color> {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return PdfHighlightColor.entries.associateWith {
|
||||
val defaultArgb = it.color.toArgb()
|
||||
val savedArgb = prefs.getInt("custom_highlight_${it.name}", defaultArgb)
|
||||
Color(savedArgb)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCustomHighlightColors(context: Context, colors: Map<PdfHighlightColor, Color>) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit {
|
||||
colors.forEach { (colorEnum, color) ->
|
||||
putInt("custom_highlight_${colorEnum.name}", color.toArgb())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putBoolean(PDF_KEEP_SCREEN_ON_KEY, isEnabled) }
|
||||
|
|
@ -1091,7 +1114,7 @@ private data class DocumentCacheItem(
|
|||
)
|
||||
|
||||
private class DocumentCache(val maxSize: Int = 3) {
|
||||
val cache = object : android.util.LruCache<String, DocumentCacheItem>(maxSize) {
|
||||
val cache = object : LruCache<String, DocumentCacheItem>(maxSize) {
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: String,
|
||||
|
|
@ -1189,7 +1212,7 @@ fun PdfViewerScreen(
|
|||
val effectiveFileType = uiState.selectedFileType ?: FileType.PDF
|
||||
|
||||
var showNewTabSheet by remember { mutableStateOf(false) }
|
||||
val sheetState = androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
|
||||
val isTabsEnabled = uiState.isTabsEnabled
|
||||
val openTabs = uiState.openTabs
|
||||
|
|
@ -1280,6 +1303,16 @@ fun PdfViewerScreen(
|
|||
var isEditMode by rememberSaveable { mutableStateOf(false) }
|
||||
var isDockMinimized by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var pendingNoteForNewHighlight by remember { mutableStateOf(false) }
|
||||
var highlightToNoteId by remember { mutableStateOf<String?>(null) }
|
||||
val onNoteRequested: (String?) -> Unit = { id ->
|
||||
if (id != null) {
|
||||
highlightToNoteId = id
|
||||
} else {
|
||||
pendingNoteForNewHighlight = true
|
||||
}
|
||||
}
|
||||
|
||||
val isDrawingActive by remember(isEditMode, isDockMinimized) {
|
||||
derivedStateOf { isEditMode && !isDockMinimized }
|
||||
}
|
||||
|
|
@ -1394,6 +1427,10 @@ fun PdfViewerScreen(
|
|||
|
||||
val (initialDockLocation, initialDockOffset) = remember(context) { loadDockState(context) }
|
||||
|
||||
var customHighlightColors by remember { mutableStateOf(loadCustomHighlightColors(context)) }
|
||||
var showHighlightColorPicker by remember { mutableStateOf(false) }
|
||||
var highlightColorPickerInitialSlot by remember { mutableStateOf(PdfHighlightColor.YELLOW) }
|
||||
|
||||
var dockLocation by remember { mutableStateOf(initialDockLocation) }
|
||||
var dockOffset by remember { mutableStateOf(initialDockOffset) }
|
||||
var snapPreviewLocation by remember { mutableStateOf<DockLocation?>(null) }
|
||||
|
|
@ -1834,6 +1871,10 @@ fun PdfViewerScreen(
|
|||
withContext(Dispatchers.Main) {
|
||||
userHighlights.add(newHighlight)
|
||||
Timber.tag("PdfExportDebug").d("userHighlights now contains ${userHighlights.size} items.")
|
||||
if (pendingNoteForNewHighlight) {
|
||||
pendingNoteForNewHighlight = false
|
||||
highlightToNoteId = newHighlight.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4094,61 +4135,125 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
var showDeleteConfirmDialogFor by remember {
|
||||
mutableStateOf<PdfUserHighlight?>(null)
|
||||
}
|
||||
val sortedHighlights = remember(userHighlights.toList()) {
|
||||
userHighlights.sortedBy { it.pageIndex }
|
||||
}
|
||||
var showDeleteConfirmDialogFor by remember { mutableStateOf<PdfUserHighlight?>(null) }
|
||||
var filterWithNotesOnly by remember { mutableStateOf(false) }
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(
|
||||
items = sortedHighlights,
|
||||
key = { _, highlight -> highlight.id }
|
||||
) { _, highlight ->
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = highlight.text.ifBlank { "Highlighted section" },
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = highlight.color.color.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
"Page ${highlight.pageIndex + 1}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(
|
||||
onClick = { showDeleteConfirmDialogFor = highlight }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete highlight",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = !filterWithNotesOnly,
|
||||
onClick = { filterWithNotesOnly = false },
|
||||
label = { Text("All") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = filterWithNotesOnly,
|
||||
onClick = { filterWithNotesOnly = true },
|
||||
label = { Text("With Notes") }
|
||||
)
|
||||
}
|
||||
|
||||
val filteredHighlights = if (filterWithNotesOnly) {
|
||||
userHighlights.filter { !it.note.isNullOrBlank() }
|
||||
} else {
|
||||
userHighlights.toList()
|
||||
}
|
||||
|
||||
val sortedHighlights = remember(filteredHighlights) {
|
||||
filteredHighlights.sortedBy { it.pageIndex }
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(
|
||||
items = sortedHighlights,
|
||||
key = { _, highlight -> highlight.id }
|
||||
) { _, highlight ->
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = highlight.text.ifBlank { "Highlighted section" },
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
drawerState.close()
|
||||
if (displayMode == DisplayMode.PAGINATION) {
|
||||
pagerState.scrollToPage(highlight.pageIndex)
|
||||
} else {
|
||||
verticalReaderState.scrollToPage(highlight.pageIndex)
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val displayColor = customHighlightColors[highlight.color] ?: highlight.color.color
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(displayColor, CircleShape)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Page ${highlight.pageIndex + 1}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (!highlight.note.isNullOrBlank()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = highlight.note,
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontStyle = FontStyle.Italic),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Box {
|
||||
var highlightMenuExpanded by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { highlightMenuExpanded = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "Options")
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = highlightMenuExpanded,
|
||||
onDismissRequest = { highlightMenuExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (highlight.note.isNullOrBlank()) "Add Note" else "Edit Note") },
|
||||
onClick = {
|
||||
onNoteRequested(highlight.id)
|
||||
highlightMenuExpanded = false
|
||||
coroutineScope.launch { drawerState.close() }
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Delete") },
|
||||
onClick = {
|
||||
showDeleteConfirmDialogFor = highlight
|
||||
highlightMenuExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
drawerState.close()
|
||||
if (displayMode == DisplayMode.PAGINATION) {
|
||||
pagerState.scrollToPage(highlight.pageIndex)
|
||||
} else {
|
||||
verticalReaderState.scrollToPage(highlight.pageIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4436,6 +4541,11 @@ fun PdfViewerScreen(
|
|||
totalPages = totalDisplayPages,
|
||||
activeTheme = activeTheme,
|
||||
isScrollLocked = isScrollLocked,
|
||||
customHighlightColors = customHighlightColors,
|
||||
onPaletteClick = {
|
||||
highlightColorPickerInitialSlot = PdfHighlightColor.YELLOW
|
||||
showHighlightColorPicker = true
|
||||
},
|
||||
onScaleChanged = { newScale ->
|
||||
if (pagerState.currentPage == pageIndex) {
|
||||
currentPageScale = newScale
|
||||
|
|
@ -4506,6 +4616,7 @@ fun PdfViewerScreen(
|
|||
onHighlightAdd = onHighlightAdd,
|
||||
onHighlightUpdate = onHighlightUpdate,
|
||||
onHighlightDelete = onHighlightDelete,
|
||||
onNoteRequested = onNoteRequested,
|
||||
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
||||
activeToolThickness = currentStrokeWidthState,
|
||||
onTwoFingerSwipe = { direction ->
|
||||
|
|
@ -4817,6 +4928,8 @@ fun PdfViewerScreen(
|
|||
pdfDocument = docHolder,
|
||||
activeTheme = activeTheme,
|
||||
isScrollLocked = isScrollLocked,
|
||||
customHighlightColors = customHighlightColors,
|
||||
onPaletteClick = { showHighlightColorPicker = true },
|
||||
totalPages = totalDisplayPages,
|
||||
pageAspectRatios = ratiosHolder,
|
||||
virtualPages = virtualPages,
|
||||
|
|
@ -4841,6 +4954,7 @@ fun PdfViewerScreen(
|
|||
onHighlightAdd = onHighlightAdd,
|
||||
onHighlightUpdate = onHighlightUpdate,
|
||||
onHighlightDelete = onHighlightDelete,
|
||||
onNoteRequested = onNoteRequested,
|
||||
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
||||
activeToolThickness = currentStrokeWidthState,
|
||||
onLinkClicked = onLinkClickedStable,
|
||||
|
|
@ -6999,7 +7113,7 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
if (showNewTabSheet) {
|
||||
androidx.compose.material3.ModalBottomSheet(
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showNewTabSheet = false },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
|
|
@ -7246,6 +7360,84 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (highlightToNoteId != null) {
|
||||
val targetHighlight = userHighlights.find { it.id == highlightToNoteId }
|
||||
if (targetHighlight != null) {
|
||||
val effectiveBg = if (activeTheme.backgroundColor == Color.Unspecified) MaterialTheme.colorScheme.surface else activeTheme.backgroundColor
|
||||
val effectiveText = if (activeTheme.textColor == Color.Unspecified) MaterialTheme.colorScheme.onSurface else activeTheme.textColor
|
||||
|
||||
PdfAnnotationBottomSheet(
|
||||
highlight = targetHighlight,
|
||||
effectiveBg = effectiveBg,
|
||||
effectiveText = effectiveText,
|
||||
customHighlightColors = customHighlightColors,
|
||||
onPaletteClick = {
|
||||
highlightColorPickerInitialSlot = targetHighlight.color
|
||||
showHighlightColorPicker = true
|
||||
},
|
||||
onColorChange = { newColor ->
|
||||
onHighlightUpdate(
|
||||
targetHighlight.id,
|
||||
newColor
|
||||
)
|
||||
},
|
||||
onDismiss = { highlightToNoteId = null },
|
||||
onSave = { noteText ->
|
||||
val index =
|
||||
userHighlights.indexOfFirst { it.id == targetHighlight.id }
|
||||
if (index != -1) {
|
||||
userHighlights[index] =
|
||||
targetHighlight.copy(note = noteText.takeIf { it.isNotBlank() })
|
||||
}
|
||||
highlightToNoteId = null
|
||||
},
|
||||
onDelete = {
|
||||
onHighlightDelete(targetHighlight.id)
|
||||
highlightToNoteId = null
|
||||
},
|
||||
onCopy = {
|
||||
val clip = ClipData.newPlainText(
|
||||
"Copied Text",
|
||||
targetHighlight.text
|
||||
)
|
||||
clipboardManager.setText(
|
||||
androidx.compose.ui.text.AnnotatedString(
|
||||
targetHighlight.text
|
||||
)
|
||||
)
|
||||
highlightToNoteId = null
|
||||
},
|
||||
onDictionary = {
|
||||
onDictionaryLookupStable(targetHighlight.text)
|
||||
highlightToNoteId = null
|
||||
},
|
||||
onTranslate = {
|
||||
onTranslateTextStable(targetHighlight.text)
|
||||
highlightToNoteId = null
|
||||
},
|
||||
onSearch = {
|
||||
onSearchTextStable(targetHighlight.text)
|
||||
highlightToNoteId = null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
highlightToNoteId = null
|
||||
}
|
||||
}
|
||||
|
||||
if (showHighlightColorPicker) {
|
||||
HighlightColorPickerDialog(
|
||||
initialColors = customHighlightColors,
|
||||
initialSelection = highlightColorPickerInitialSlot,
|
||||
onDismiss = { showHighlightColorPicker = false },
|
||||
onSave = { newColors ->
|
||||
customHighlightColors = newColors
|
||||
saveCustomHighlightColors(context, newColors)
|
||||
showHighlightColorPicker = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showThemePanel) {
|
||||
ReaderThemePanel(
|
||||
isVisible = true,
|
||||
|
|
|
|||
|
|
@ -224,6 +224,10 @@ object HighlightSerializer {
|
|||
obj.put("rangeStart", h.range.first)
|
||||
obj.put("rangeEnd", h.range.second)
|
||||
|
||||
if (!h.note.isNullOrBlank()) {
|
||||
obj.put("note", h.note)
|
||||
}
|
||||
|
||||
val boundsArray = JSONArray()
|
||||
h.bounds.forEach { r ->
|
||||
val rObj = JSONObject()
|
||||
|
|
@ -264,7 +268,8 @@ object HighlightSerializer {
|
|||
bounds = bounds,
|
||||
color = try { PdfHighlightColor.valueOf(obj.getString("color")) } catch(_: Exception) { PdfHighlightColor.YELLOW },
|
||||
text = obj.optString("text", ""),
|
||||
range = Pair(obj.optInt("rangeStart", 0), obj.optInt("rangeEnd", 0))
|
||||
range = Pair(obj.optInt("rangeStart", 0), obj.optInt("rangeEnd", 0)),
|
||||
note = obj.optString("note", null).takeIf { !it.isNullOrBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue