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
|
|
@ -423,8 +423,8 @@
|
|||
|
||||
var cfiToReport = rawCfi;
|
||||
|
||||
if (rawCfi && rawCfi.includes("|")) {
|
||||
var cfiParts = rawCfi.split("|");
|
||||
if (rawCfi && rawCfi.includes(";;")) {
|
||||
var cfiParts = rawCfi.split(";;");
|
||||
cfiToReport = cfiParts[cfiParts.length - 1];
|
||||
console.log("HandleInteraction: Multi-CFI detected on single span. Reporting top layer: " + cfiToReport);
|
||||
}
|
||||
|
|
@ -2051,31 +2051,30 @@
|
|||
let idx = parseInt(div.dataset.chunkIndex, 10);
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// Check if we have data for this chunk at specific index
|
||||
if (!this.chunksData[idx]) {
|
||||
if (window.ContentBridge && window.ContentBridge.requestChunk) {
|
||||
window.ContentBridge.requestChunk(idx);
|
||||
}
|
||||
} else if (div.innerHTML === "") {
|
||||
// Restore content from cache
|
||||
let oldHeight = div.getBoundingClientRect().height;
|
||||
div.innerHTML = this.chunksData[idx]; // FIX: Access by index
|
||||
div.style.height = ""; // Allow auto height
|
||||
div.innerHTML = this.chunksData[idx];
|
||||
div.style.height = "";
|
||||
|
||||
let newHeight = div.getBoundingClientRect().height;
|
||||
this.chunkHeights[idx] = newHeight; // Update cached height
|
||||
this.chunkHeights[idx] = newHeight;
|
||||
|
||||
// Adjust scroll if this expansion happened above our viewport
|
||||
if (div.getBoundingClientRect().top < 0) {
|
||||
scrollAdjust += (newHeight - oldHeight);
|
||||
}
|
||||
if (window.CURRENT_HIGHLIGHTS) {
|
||||
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unload content to save memory/DOM weight
|
||||
if (div.innerHTML !== "") {
|
||||
let oldHeight = div.getBoundingClientRect().height;
|
||||
this.chunkHeights[idx] = oldHeight;
|
||||
div.style.height = oldHeight + "px"; // Fix height to placeholder
|
||||
div.style.height = oldHeight + "px";
|
||||
div.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -2115,6 +2114,9 @@
|
|||
if (div.getBoundingClientRect().bottom < 0) {
|
||||
window.scrollBy(0, newHeight - oldHeight);
|
||||
}
|
||||
if (window.CURRENT_HIGHLIGHTS) {
|
||||
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.checkImagesForDiagnosis) {
|
||||
|
|
@ -2133,7 +2135,7 @@
|
|||
|
||||
allSpans.forEach((span) => {
|
||||
var currentCfiAttr = span.getAttribute("data-cfi") || "";
|
||||
var cfis = currentCfiAttr.split("|");
|
||||
var cfis = currentCfiAttr.split(";;");
|
||||
|
||||
if (cfis.includes(cfi)) {
|
||||
var classesToRemove = [];
|
||||
|
|
@ -2288,10 +2290,10 @@
|
|||
|
||||
if (parent && parent.tagName === "SPAN" && parent.classList.contains(className)) {
|
||||
var currentCfi = parent.getAttribute("data-cfi") || "";
|
||||
var cfiList = currentCfi.split("|");
|
||||
var cfiList = currentCfi.split(";;");
|
||||
|
||||
if (!cfiList.includes(newCfi)) {
|
||||
parent.setAttribute("data-cfi", currentCfi + "|" + newCfi);
|
||||
parent.setAttribute("data-cfi", currentCfi ? (currentCfi + ";;" + newCfi) : newCfi);
|
||||
}
|
||||
} else {
|
||||
if (node.nodeValue.trim().length === 0) return;
|
||||
|
|
@ -2366,7 +2368,7 @@
|
|||
|
||||
allSpans.forEach((span) => {
|
||||
var currentCfiAttr = span.getAttribute("data-cfi") || "";
|
||||
var cfiList = currentCfiAttr.split("|");
|
||||
var cfiList = currentCfiAttr.split(";;");
|
||||
|
||||
// Detailed check for match
|
||||
if (cfiList.includes(cfiToRemove)) {
|
||||
|
|
@ -2397,15 +2399,14 @@
|
|||
parent.normalize();
|
||||
removedCount++;
|
||||
} else {
|
||||
// CASE 2: Overlapping highlight -> Update data-cfi
|
||||
console.log(`$ {
|
||||
HL_LOG_TAG
|
||||
}
|
||||
|
||||
: -> Updating span (remaining CFIs: $ {
|
||||
newCfiList.join('|')
|
||||
newCfiList.join(';;')
|
||||
}).`);
|
||||
span.setAttribute("data-cfi", newCfiList.join("|"));
|
||||
span.setAttribute("data-cfi", newCfiList.join(";;"));
|
||||
|
||||
if (optionalCssClass) {
|
||||
console.log(`$ {
|
||||
|
|
@ -2497,9 +2498,16 @@
|
|||
},
|
||||
|
||||
applyHighlight: function (cfi, text, cssClass) {
|
||||
// "Healed" Apply Logic: Checks text equality before applying
|
||||
try {
|
||||
if (document.querySelector(`span[data-cfi='${cfi}']`)) return;
|
||||
var alreadyApplied = false;
|
||||
var spans = document.querySelectorAll(`span[data-cfi]`);
|
||||
for (var i = 0; i < spans.length; i++) {
|
||||
if ((spans[i].getAttribute("data-cfi") || "").split(";;").includes(cfi)) {
|
||||
alreadyApplied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadyApplied) return;
|
||||
|
||||
const location = window.getNodeAndOffsetFromCfi(cfi);
|
||||
if (!location || !location.node) return;
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ import androidx.media3.common.util.UnstableApi
|
|||
import com.aryan.reader.epubreader.PREF_CUSTOM_THEMES
|
||||
import com.aryan.reader.epubreader.PREF_READER_THEME
|
||||
import com.aryan.reader.paginatedreader.TtsChunk
|
||||
import com.aryan.reader.pdf.PdfHighlightColor
|
||||
import com.aryan.reader.tts.GOOGLE_TTS_SPEAKERS
|
||||
import com.aryan.reader.tts.SpeakerSamplePlayer
|
||||
import com.aryan.reader.tts.TtsPlaybackManager
|
||||
|
|
@ -2616,4 +2617,207 @@ fun ColorSlider(color: Color, onColorChanged: (Color) -> Unit) {
|
|||
Slider(value = color.green, onValueChange = { onColorChanged(color.copy(green = it)) }, colors = androidx.compose.material3.SliderDefaults.colors(thumbColor = Color.Green, activeTrackColor = Color.Green), modifier = Modifier.weight(1f))
|
||||
Slider(value = color.blue, onValueChange = { onColorChanged(color.copy(blue = it)) }, colors = androidx.compose.material3.SliderDefaults.colors(thumbColor = Color.Blue, activeTrackColor = Color.Blue), modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HighlightColorPickerDialog(
|
||||
initialColors: Map<PdfHighlightColor, Color>,
|
||||
initialSelection: PdfHighlightColor = PdfHighlightColor.YELLOW,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (Map<PdfHighlightColor, Color>) -> Unit
|
||||
) {
|
||||
var currentColors by remember { mutableStateOf(initialColors) }
|
||||
var selectedSlot by remember { mutableStateOf(initialSelection) }
|
||||
|
||||
val initialActiveColor = currentColors[selectedSlot] ?: selectedSlot.color
|
||||
val initialHsv = remember(initialActiveColor) {
|
||||
val hsv = FloatArray(3)
|
||||
android.graphics.Color.colorToHSV(initialActiveColor.toArgb(), hsv)
|
||||
hsv
|
||||
}
|
||||
|
||||
var hue by remember { mutableFloatStateOf(initialHsv[0]) }
|
||||
var saturation by remember { mutableFloatStateOf(initialHsv[1]) }
|
||||
var value by remember { mutableFloatStateOf(initialHsv[2]) }
|
||||
|
||||
LaunchedEffect(selectedSlot) {
|
||||
val color = currentColors[selectedSlot] ?: selectedSlot.color
|
||||
val hsv = FloatArray(3)
|
||||
android.graphics.Color.colorToHSV(color.toArgb(), hsv)
|
||||
hue = hsv[0]
|
||||
saturation = hsv[1]
|
||||
value = hsv[2]
|
||||
}
|
||||
|
||||
val currentColor by remember {
|
||||
derivedStateOf {
|
||||
val hsv = floatArrayOf(hue, saturation, value)
|
||||
Color(android.graphics.Color.HSVToColor(255, hsv))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentColor) {
|
||||
currentColors = currentColors + (selectedSlot to currentColor)
|
||||
}
|
||||
|
||||
fun updateFromColor(color: Color) {
|
||||
val hsv = FloatArray(3)
|
||||
android.graphics.Color.colorToHSV(color.toArgb(), hsv)
|
||||
hue = hsv[0]
|
||||
saturation = hsv[1]
|
||||
value = hsv[2]
|
||||
}
|
||||
|
||||
androidx.compose.ui.window.Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = androidx.compose.ui.window.DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color(0xFF2C2C2C),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color(0xFF3E3E3E), RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Customize Highlights",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
PdfHighlightColor.entries.forEach { slot ->
|
||||
val slotColor = currentColors[slot] ?: slot.color
|
||||
val isSelected = selectedSlot == slot
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(slotColor)
|
||||
.clickable { selectedSlot = slot }
|
||||
.border(
|
||||
width = if (isSelected) 3.dp else 1.dp,
|
||||
color = if (isSelected) Color.White else Color.Gray,
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = if (slotColor.luminance() > 0.5f) Color.Black else Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
SpectrumBox(
|
||||
hue = hue,
|
||||
saturation = saturation,
|
||||
currentColor = currentColor,
|
||||
onHueSatChanged = { h, s -> hue = h; saturation = s },
|
||||
modifier = Modifier.fillMaxWidth().height(220.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
BrightnessSlider(
|
||||
hue = hue,
|
||||
saturation = saturation,
|
||||
value = value,
|
||||
onValueChanged = { value = it },
|
||||
modifier = Modifier.fillMaxWidth().height(24.dp).clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ColorComparePill(
|
||||
oldColor = selectedSlot.color,
|
||||
newColor = currentColor,
|
||||
modifier = Modifier.width(64.dp).height(36.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1.6f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("HEX", color = Color.Gray, fontSize = 12.sp, maxLines = 1)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.weight(2.4f),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
RgbInputColumn(label = "R", value = currentColor.red,
|
||||
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
RgbInputColumn(label = "G", value = currentColor.green,
|
||||
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
RgbInputColumn(label = "B", value = currentColor.blue,
|
||||
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = { updateFromColor(selectedSlot.color) }) {
|
||||
Text("Reset", color = Color(0xFFFF5252))
|
||||
}
|
||||
Row {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel", color = Color.Gray)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = { onSave(currentColors) },
|
||||
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Save", color = Color.Black, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +82,6 @@ import androidx.compose.ui.window.Popup
|
|||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.aryan.reader.ReaderTexture
|
||||
import com.aryan.reader.paginatedreader.PaginatedTextSelectionMenu
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
|
@ -280,7 +279,9 @@ private data class CustomMenuState(
|
|||
val selectionBounds: Rect,
|
||||
val finishActionModeCallback: () -> Unit,
|
||||
val cfi: String? = null,
|
||||
val isExistingHighlight: Boolean = false
|
||||
val isExistingHighlight: Boolean = false,
|
||||
val note: String? = null,
|
||||
val selectedColor: HighlightColor? = null
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
@ -344,6 +345,7 @@ fun ChapterWebView(
|
|||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onNoteRequested: (String?) -> Unit,
|
||||
onContentReadyForSummarization: suspend (String) -> Unit,
|
||||
currentFontFamily: ReaderFont,
|
||||
customFontPath: String? = null,
|
||||
|
|
@ -368,6 +370,7 @@ fun ChapterWebView(
|
|||
val jsToInject = remember(context) { getJsToInject(context) }
|
||||
|
||||
var showPaletteManager by remember { mutableStateOf(false) }
|
||||
val latestUserHighlights by rememberUpdatedState(userHighlights)
|
||||
|
||||
val textureBase64 by remember(activeTextureId) {
|
||||
mutableStateOf(
|
||||
|
|
@ -461,7 +464,7 @@ fun ChapterWebView(
|
|||
Timber.d(
|
||||
"InteractiveWebView factory for $chapterTitle (Key: $key), isDarkTheme: $isDarkTheme, initialScroll: $initialScrollTarget"
|
||||
)
|
||||
val webView = InteractiveWebView(
|
||||
@Suppress("unused") val webView = InteractiveWebView(
|
||||
context = ctx,
|
||||
onSingleTap = onTap,
|
||||
onPotentialScroll = onPotentialScroll,
|
||||
|
|
@ -511,32 +514,11 @@ fun ChapterWebView(
|
|||
onClickCallback = { cfi, text, left, top, right, bottom ->
|
||||
this.post {
|
||||
onHighlightClicked()
|
||||
|
||||
val densityValue = density.density
|
||||
val locationOnScreen = IntArray(2)
|
||||
this.getLocationOnScreen(locationOnScreen)
|
||||
val xOffset = locationOnScreen[0]
|
||||
val yOffset = locationOnScreen[1]
|
||||
|
||||
val rect = Rect(
|
||||
(left * densityValue).toInt() + xOffset,
|
||||
(top * densityValue).toInt() + yOffset,
|
||||
(right * densityValue).toInt() + xOffset,
|
||||
(bottom * densityValue).toInt() + yOffset
|
||||
)
|
||||
|
||||
customMenuState = CustomMenuState(
|
||||
selectedText = text,
|
||||
selectionBounds = rect,
|
||||
finishActionModeCallback = {
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||
null
|
||||
)
|
||||
},
|
||||
cfi = cfi,
|
||||
isExistingHighlight = true
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||
null
|
||||
)
|
||||
onNoteRequested(cfi)
|
||||
}
|
||||
}
|
||||
), "HighlightBridge"
|
||||
|
|
@ -846,7 +828,13 @@ fun ChapterWebView(
|
|||
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');",
|
||||
null
|
||||
)
|
||||
}, modifier = Modifier.fillMaxSize()
|
||||
|
||||
val escapedHighlights = escapeJsString(highlightsJson)
|
||||
webView.evaluateJavascript(
|
||||
"javascript:window.CURRENT_HIGHLIGHTS = '${escapedHighlights}'; window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);",
|
||||
null
|
||||
)
|
||||
}, modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +977,19 @@ fun ChapterWebView(
|
|||
}
|
||||
customMenuState = null
|
||||
},
|
||||
onHighlight = null, // Highlight handles itself above in the Colors Row
|
||||
onNote = {
|
||||
if (state.isExistingHighlight && state.cfi != null) {
|
||||
onNoteRequested(state.cfi)
|
||||
} else {
|
||||
onNoteRequested(null)
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.createUserHighlight('${HighlightColor.YELLOW.cssClass}', '${HighlightColor.YELLOW.id}');", null
|
||||
)
|
||||
}
|
||||
state.finishActionModeCallback()
|
||||
customMenuState = null
|
||||
},
|
||||
onHighlight = null,
|
||||
onTts = {
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.TtsBridgeHelper.extractAndRelayTextFromSelection();",
|
||||
|
|
@ -1006,21 +1006,13 @@ fun ChapterWebView(
|
|||
onDelete = if (state.isExistingHighlight && state.cfi != null) {
|
||||
{
|
||||
val highlightToDelete = userHighlights.find { h ->
|
||||
h.cfi == state.cfi || h.cfi.split("|")
|
||||
.contains(state.cfi)
|
||||
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
||||
}
|
||||
if (highlightToDelete != null) {
|
||||
val cssClassToDelete = highlightToDelete.color.cssClass
|
||||
val allCfiParts = highlightToDelete.cfi.split("|")
|
||||
allCfiParts.forEach { partCfi ->
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${
|
||||
escapeJsString(
|
||||
partCfi
|
||||
)
|
||||
}', '$cssClassToDelete');", null
|
||||
)
|
||||
}
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(highlightToDelete.cfi)}', '$cssClassToDelete');", null
|
||||
)
|
||||
onHighlightDeleted(highlightToDelete.cfi)
|
||||
}
|
||||
state.finishActionModeCallback()
|
||||
|
|
@ -1028,7 +1020,10 @@ fun ChapterWebView(
|
|||
}
|
||||
} else null,
|
||||
isProUser = isProUser,
|
||||
isOss = isOss)
|
||||
isOss = isOss,
|
||||
existingNote = state.note,
|
||||
selectedColor = state.selectedColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import timber.log.Timber
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -44,15 +45,22 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import com.aryan.reader.R
|
||||
|
|
@ -96,7 +104,8 @@ data class UserHighlight(
|
|||
val cfi: String,
|
||||
val text: String,
|
||||
val color: HighlightColor,
|
||||
val chapterIndex: Int
|
||||
val chapterIndex: Int,
|
||||
val note: String? = null
|
||||
)
|
||||
|
||||
fun escapeJsString(value: String): String {
|
||||
|
|
@ -182,6 +191,7 @@ fun saveHighlightsToPrefs(context: Context, bookTitle: String, highlights: List<
|
|||
put("text", h.text)
|
||||
put("colorId", h.color.id)
|
||||
put("chapterIndex", h.chapterIndex)
|
||||
put("note", h.note ?: "")
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
}
|
||||
|
|
@ -200,13 +210,15 @@ fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List<UserHighl
|
|||
val obj = jsonArray.getJSONObject(i)
|
||||
val colorId = obj.getString("colorId")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val noteStr = obj.optString("note", "")
|
||||
list.add(
|
||||
UserHighlight(
|
||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||
cfi = obj.getString("cfi"),
|
||||
text = obj.getString("text"),
|
||||
color = color,
|
||||
chapterIndex = obj.getInt("chapterIndex")
|
||||
chapterIndex = obj.getInt("chapterIndex"),
|
||||
note = noteStr.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -225,13 +237,15 @@ fun parseHighlightsJson(jsonString: String?): List<UserHighlight> {
|
|||
val obj = jsonArray.getJSONObject(i)
|
||||
val colorId = obj.getString("colorId")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val noteStr = obj.optString("note", "")
|
||||
list.add(
|
||||
UserHighlight(
|
||||
id = obj.optString("id", java.util.UUID.randomUUID().toString()),
|
||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||
cfi = obj.getString("cfi"),
|
||||
text = obj.getString("text"),
|
||||
color = color,
|
||||
chapterIndex = obj.getInt("chapterIndex")
|
||||
chapterIndex = obj.getInt("chapterIndex"),
|
||||
note = noteStr.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -250,6 +264,7 @@ fun highlightsToJson(highlights: List<UserHighlight>): String {
|
|||
put("text", h.text)
|
||||
put("colorId", h.color.id)
|
||||
put("chapterIndex", h.chapterIndex)
|
||||
put("note", h.note ?: "")
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
}
|
||||
|
|
@ -271,7 +286,7 @@ fun processAndAddHighlight(
|
|||
newColor: HighlightColor,
|
||||
chapterIndex: Int,
|
||||
currentList: MutableList<UserHighlight>
|
||||
) {
|
||||
): String {
|
||||
val newParts = newCfi.split('|')
|
||||
val newStartFull = newParts.first()
|
||||
val newEndFull = newParts.last()
|
||||
|
|
@ -286,10 +301,11 @@ fun processAndAddHighlight(
|
|||
var finalEndPath = newEndPath
|
||||
var finalEndOffset = newEndOffset
|
||||
var finalText = newText
|
||||
var finalNote: String? = null
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val existing = iterator.next()
|
||||
if (existing.chapterIndex != chapterIndex || existing.color != newColor) continue
|
||||
if (existing.chapterIndex != chapterIndex) continue
|
||||
|
||||
val exParts = existing.cfi.split('|')
|
||||
val exStartFull = exParts.first()
|
||||
|
|
@ -317,6 +333,7 @@ fun processAndAddHighlight(
|
|||
|
||||
if (!isDisjoint) {
|
||||
iterator.remove()
|
||||
if (existing.note != null && finalNote == null) finalNote = existing.note
|
||||
val unionStartCmp = comparePaths(finalStartPath, exStartPath)
|
||||
if (unionStartCmp > 0 || (unionStartCmp == 0 && finalStartOffset > exStartOffset)) {
|
||||
finalStartPath = exStartPath
|
||||
|
|
@ -331,12 +348,15 @@ fun processAndAddHighlight(
|
|||
}
|
||||
}
|
||||
|
||||
val finalCfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset"
|
||||
currentList.add(UserHighlight(
|
||||
cfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset",
|
||||
cfi = finalCfi,
|
||||
text = finalText,
|
||||
color = newColor,
|
||||
chapterIndex = chapterIndex
|
||||
chapterIndex = chapterIndex,
|
||||
note = finalNote
|
||||
))
|
||||
return finalCfi
|
||||
}
|
||||
|
||||
// --- UI Components ---
|
||||
|
|
@ -479,4 +499,459 @@ fun PaletteManagerDialog(
|
|||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnnotationBottomSheet(
|
||||
highlight: UserHighlight,
|
||||
effectiveBg: Color,
|
||||
effectiveText: Color,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
onColorChange: (HighlightColor) -> Unit,
|
||||
onOpenPaletteManager: () -> 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, // Matches user theme
|
||||
contentColor = effectiveText,
|
||||
contentWindowInsets = { WindowInsets.navigationBars }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
// Top: Highlight Colors
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = highlight.color,
|
||||
onColorSelect = onColorChange,
|
||||
onOpenPaletteManager = onOpenPaletteManager,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// Middle: Elegant Highlight Snippet Card
|
||||
Surface(
|
||||
color = highlight.color.color.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(1.dp, highlight.color.color.copy(alpha = 0.3f)),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
// Left colored accent bar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(6.dp)
|
||||
.fillMaxHeight()
|
||||
.background(highlight.color.color)
|
||||
)
|
||||
Text(
|
||||
text = "\"${highlight.text}\"",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic),
|
||||
maxLines = 4,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
color = effectiveText.copy(alpha = 0.9f),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Action Tools Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
BottomSheetToolButton(icon = R.drawable.copy, label = "Copy", onClick = onCopy, effectiveText = effectiveText)
|
||||
BottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", onClick = onDictionary, effectiveText = effectiveText)
|
||||
BottomSheetToolButton(icon = R.drawable.translate, label = "Translate", onClick = onTranslate, effectiveText = effectiveText)
|
||||
BottomSheetToolButton(icon = R.drawable.search, label = "Search", onClick = onSearch, effectiveText = effectiveText)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Note TextField
|
||||
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))
|
||||
|
||||
// Bottom Actions
|
||||
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 BottomSheetToolButton(
|
||||
icon: Int,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
effectiveText: Color
|
||||
) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaginatedTextSelectionMenu(
|
||||
onCopy: () -> Unit,
|
||||
onSelectAll: (() -> Unit)?,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
onHighlight: ((HighlightColor) -> Unit)?,
|
||||
onNote: (() -> Unit)? = null,
|
||||
onDelete: (() -> Unit)?,
|
||||
onTts: (() -> Unit)?,
|
||||
@Suppress("unused") isProUser: Boolean,
|
||||
@Suppress("unused") isOss: Boolean,
|
||||
activeHighlightPalette: List<HighlightColor> = emptyList(),
|
||||
onOpenPaletteManager: (() -> Unit)? = null
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shadowElevation = 6.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 200.dp)) {
|
||||
if (onHighlight != null) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = null,
|
||||
onColorSelect = onHighlight,
|
||||
onOpenPaletteManager = onOpenPaletteManager
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
val actions = mutableListOf<MenuActionItem>()
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = onCopy))
|
||||
if (onTts != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts))
|
||||
}
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = onDictionary))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch))
|
||||
|
||||
if (onNote != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = "Note", onClick = onNote))
|
||||
}
|
||||
|
||||
if (onSelectAll != null) {
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = onSelectAll))
|
||||
}
|
||||
if (onDelete != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = onDelete, isError = true))
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(bottom = 4.dp)) {
|
||||
actions.chunked(3).forEach { rowActions ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
rowActions.forEach { action ->
|
||||
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { action.onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (action.imageVector != null) {
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
} else if (action.iconRes != null) {
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
|
||||
}
|
||||
}
|
||||
repeat(3 - rowActions.size) {
|
||||
Spacer(modifier = Modifier.width(64.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MenuActionItem(
|
||||
val iconRes: Int? = null,
|
||||
val imageVector: androidx.compose.ui.graphics.vector.ImageVector? = null,
|
||||
val label: String,
|
||||
val onClick: () -> Unit,
|
||||
val isError: Boolean = false
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun HighlightColorRow(
|
||||
modifier: Modifier = Modifier,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
selectedColor: HighlightColor? = null,
|
||||
onColorSelect: (HighlightColor) -> Unit,
|
||||
onOpenPaletteManager: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
activeHighlightPalette.forEach { colorEnum ->
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape) // 1. Clip shape for ripple
|
||||
.background(colorEnum.color) // 2. Apply background
|
||||
.clickable {
|
||||
Timber.d("HighlightColorRow: Color clicked -> ${colorEnum.name}")
|
||||
onColorSelect(colorEnum)
|
||||
} // 3. Add clickable (ripple)
|
||||
.border( // 4. Add border on top
|
||||
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 (colorEnum == HighlightColor.WHITE || colorEnum == HighlightColor.YELLOW) Color.Black else Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onOpenPaletteManager != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
SpectrumButton(
|
||||
onClick = onOpenPaletteManager,
|
||||
size = 32.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaginatedTextSelectionMenu(
|
||||
onCopy: () -> Unit,
|
||||
onSelectAll: (() -> Unit)?,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
onHighlight: ((HighlightColor) -> Unit)?,
|
||||
onNote: (() -> Unit)? = null,
|
||||
onDelete: (() -> Unit)?,
|
||||
onTts: (() -> Unit)?,
|
||||
@Suppress("unused") isProUser: Boolean,
|
||||
@Suppress("unused") isOss: Boolean,
|
||||
activeHighlightPalette: List<HighlightColor> = emptyList(),
|
||||
onOpenPaletteManager: (() -> Unit)? = null,
|
||||
existingNote: String? = null,
|
||||
selectedColor: HighlightColor? = null
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shadowElevation = 6.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 200.dp)) {
|
||||
// 1. Colors Row
|
||||
if (onHighlight != null) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = selectedColor,
|
||||
onColorSelect = onHighlight,
|
||||
onOpenPaletteManager = onOpenPaletteManager
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
// 2. Improved Comment/Note View
|
||||
if (!existingNote.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 = existingNote,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
val actions = mutableListOf<MenuActionItem>()
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = onCopy))
|
||||
if (onTts != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts))
|
||||
}
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = onDictionary))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch))
|
||||
|
||||
if (onNote != null) {
|
||||
val noteLabel = if (existingNote.isNullOrBlank()) "Note" else "Edit"
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
|
||||
}
|
||||
|
||||
if (onSelectAll != null) {
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = onSelectAll))
|
||||
}
|
||||
if (onDelete != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = onDelete, isError = true))
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(bottom = 4.dp)) {
|
||||
actions.chunked(3).forEach { rowActions ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
rowActions.forEach { action ->
|
||||
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.clickable { action.onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (action.imageVector != null) {
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
} else if (action.iconRes != null) {
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
|
||||
}
|
||||
}
|
||||
repeat(3 - rowActions.size) {
|
||||
Spacer(modifier = Modifier.width(64.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import androidx.compose.foundation.gestures.draggable
|
|||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -65,6 +66,7 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -213,7 +215,11 @@ fun EpubReaderDrawerSheet(
|
|||
onNavigateToHighlight: (UserHighlight) -> Unit,
|
||||
onDeleteBookmark: (Bookmark) -> Unit,
|
||||
onRenameBookmark: (Bookmark, String) -> Unit,
|
||||
onDeleteHighlight: (UserHighlight) -> Unit
|
||||
onDeleteHighlight: (UserHighlight) -> Unit,
|
||||
onEditNote: (UserHighlight) -> Unit,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
onOpenPaletteManager: () -> Unit,
|
||||
onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit
|
||||
) {
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars)
|
||||
|
|
@ -236,7 +242,7 @@ fun EpubReaderDrawerSheet(
|
|||
Tab(
|
||||
selected = drawerPagerState.currentPage == 2,
|
||||
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } },
|
||||
text = { Text(stringResource(R.string.tab_highlights)) }
|
||||
text = { Text("Annotations") }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +273,11 @@ fun EpubReaderDrawerSheet(
|
|||
userHighlights = userHighlights,
|
||||
chapters = chapters,
|
||||
onNavigateToHighlight = onNavigateToHighlight,
|
||||
onDeleteHighlight = onDeleteHighlight
|
||||
onDeleteHighlight = onDeleteHighlight,
|
||||
onEditNote = onEditNote,
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
onOpenPaletteManager = onOpenPaletteManager,
|
||||
onHighlightColorChange = onHighlightColorChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -639,12 +649,17 @@ private fun BookmarksList(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun HighlightsList(
|
||||
userHighlights: List<UserHighlight>,
|
||||
chapters: List<EpubChapter>,
|
||||
onNavigateToHighlight: (UserHighlight) -> Unit,
|
||||
onDeleteHighlight: (UserHighlight) -> Unit
|
||||
onDeleteHighlight: (UserHighlight) -> Unit,
|
||||
onEditNote: (UserHighlight) -> Unit,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
onOpenPaletteManager: () -> Unit,
|
||||
onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit
|
||||
) {
|
||||
if (userHighlights.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
|
|
@ -653,76 +668,138 @@ private fun HighlightsList(
|
|||
} else {
|
||||
var highlightMenuExpandedFor by remember { mutableStateOf<UserHighlight?>(null) }
|
||||
var showHighlightDeleteDialogFor by remember { mutableStateOf<UserHighlight?>(null) }
|
||||
var filterWithNotesOnly by remember { mutableStateOf(false) } // ADDED
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize().padding(end = 4.dp)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
items = userHighlights.sortedBy { it.chapterIndex },
|
||||
key = { it.id }
|
||||
) { highlight ->
|
||||
val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = highlight.text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(highlight.color.color, CircleShape)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = chapterTitle,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Box {
|
||||
IconButton(onClick = { highlightMenuExpandedFor = highlight }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.content_desc_options)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = highlightMenuExpandedFor == highlight,
|
||||
onDismissRequest = { highlightMenuExpandedFor = null }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.action_delete)) },
|
||||
onClick = {
|
||||
showHighlightDeleteDialogFor = highlight
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable { onNavigateToHighlight(highlight) }
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
androidx.compose.material3.FilterChip(
|
||||
selected = !filterWithNotesOnly,
|
||||
onClick = { filterWithNotesOnly = false },
|
||||
label = { Text("All") }
|
||||
)
|
||||
androidx.compose.material3.FilterChip(
|
||||
selected = filterWithNotesOnly,
|
||||
onClick = { filterWithNotesOnly = true },
|
||||
label = { Text("With Notes") }
|
||||
)
|
||||
}
|
||||
|
||||
VerticalScrollbar(
|
||||
listState = listState,
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
)
|
||||
val filteredHighlights = if (filterWithNotesOnly) {
|
||||
userHighlights.filter { !it.note.isNullOrBlank() }
|
||||
} else {
|
||||
userHighlights
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize().padding(end = 4.dp)
|
||||
) {
|
||||
items(
|
||||
items = filteredHighlights.sortedBy { it.chapterIndex },
|
||||
key = { it.id }
|
||||
) { highlight ->
|
||||
val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = highlight.text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(highlight.color.color, CircleShape)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = chapterTitle,
|
||||
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 = androidx.compose.ui.text.font.FontStyle.Italic),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Box {
|
||||
IconButton(onClick = { highlightMenuExpandedFor = highlight }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.content_desc_options)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = highlightMenuExpandedFor == highlight,
|
||||
onDismissRequest = { highlightMenuExpandedFor = null }
|
||||
) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = highlight.color,
|
||||
onColorSelect = { color ->
|
||||
onHighlightColorChange(highlight, color)
|
||||
highlightMenuExpandedFor = null
|
||||
},
|
||||
onOpenPaletteManager = {
|
||||
onOpenPaletteManager()
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (highlight.note.isNullOrBlank()) "Add Note" else "Edit Note") },
|
||||
onClick = {
|
||||
onEditNote(highlight)
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.action_delete)) },
|
||||
onClick = {
|
||||
showHighlightDeleteDialogFor = highlight
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable { onNavigateToHighlight(highlight) }
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollbar(
|
||||
listState = listState,
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
showHighlightDeleteDialogFor?.let { highlightToDelete ->
|
||||
|
|
|
|||
|
|
@ -477,6 +477,9 @@ fun EpubReaderHost(
|
|||
var sliderStartPage by remember { mutableIntStateOf(0) }
|
||||
var startPageThumbnail by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
var pendingNoteForNewHighlight by remember { mutableStateOf(false) }
|
||||
var highlightToNoteCfi by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var showJustifyWarningDialog by remember { mutableStateOf(false) }
|
||||
var isNavigatingByToc by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -1022,6 +1025,7 @@ fun EpubReaderHost(
|
|||
var showTtsSettingsSheet by remember { mutableStateOf(false) }
|
||||
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
||||
var showThemePanel by remember { mutableStateOf(false) }
|
||||
var showPaletteManager by remember { mutableStateOf(false) }
|
||||
|
||||
var currentThemeId by remember { mutableStateOf(loadReaderThemeId(context)) }
|
||||
var customThemes by remember { mutableStateOf(loadCustomThemes(context)) }
|
||||
|
|
@ -1066,6 +1070,18 @@ fun EpubReaderHost(
|
|||
}
|
||||
}
|
||||
|
||||
val onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit = { targetHighlight, newColor ->
|
||||
val index = userHighlights.indexOfFirst { it.cfi == targetHighlight.cfi }
|
||||
if (index != -1) {
|
||||
userHighlights[index] = targetHighlight.copy(color = newColor)
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL && targetHighlight.chapterIndex == currentChapterIndex) {
|
||||
val cssClass = newColor.cssClass
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.updateHighlightStyle('${escapeJsString(targetHighlight.cfi)}', '$cssClass', '${newColor.id}');"
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startTts() {
|
||||
if (isAutoScrollModeActive) {
|
||||
isAutoScrollModeActive = false
|
||||
|
|
@ -1637,13 +1653,16 @@ fun EpubReaderHost(
|
|||
drawerContent = {
|
||||
EpubReaderDrawerSheet(
|
||||
chapters = chapters,
|
||||
tableOfContents = epubBook.tableOfContents, // Pass TOC
|
||||
tableOfContents = epubBook.tableOfContents,
|
||||
activeFragmentId = activeFragmentId,
|
||||
bookmarks = bookmarks,
|
||||
userHighlights = userHighlights,
|
||||
currentChapterIndex = currentChapterIndex,
|
||||
currentChapterInPaginatedMode = currentChapterInPaginatedMode,
|
||||
renderMode = currentRenderMode,
|
||||
activeHighlightPalette = currentHighlightPalette,
|
||||
onOpenPaletteManager = { showPaletteManager = true },
|
||||
onHighlightColorChange = onHighlightColorChange,
|
||||
onNavigateToTocEntry = { entry ->
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
|
|
@ -1948,15 +1967,14 @@ fun EpubReaderHost(
|
|||
highlightToDelete.chapterIndex == currentChapterIndex) {
|
||||
|
||||
val cssClass = highlightToDelete.color.cssClass
|
||||
val cfiParts = highlightToDelete.cfi.split("|")
|
||||
|
||||
cfiParts.forEach { partCfi ->
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClass');"
|
||||
Timber.d("Executing JS removal for part: $partCfi")
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(highlightToDelete.cfi)}', '$cssClass');"
|
||||
Timber.d("Executing JS removal for highlight: ${highlightToDelete.cfi}")
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
}
|
||||
},
|
||||
onEditNote = { highlight ->
|
||||
highlightToNoteCfi = highlight.cfi
|
||||
},
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
|
@ -2238,21 +2256,25 @@ fun EpubReaderHost(
|
|||
Timber.d("Vertical Mode (Source): Creating Highlight. CFI: $cfi")
|
||||
Timber.d("Vertical Mode (Source): Text Snippet: '${text.take(50)}...'")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val existingIndex = userHighlights.indexOfFirst { it.cfi == cfi }
|
||||
|
||||
if (existingIndex != -1) {
|
||||
val existing = userHighlights[existingIndex]
|
||||
userHighlights[existingIndex] = existing.copy(color = color, text = text)
|
||||
Timber.d("Kotlin: Updated existing highlight at index $existingIndex")
|
||||
val finalCfi = processAndAddHighlight(
|
||||
newCfi = cfi,
|
||||
newText = text,
|
||||
newColor = color,
|
||||
chapterIndex = currentChapterIndex,
|
||||
currentList = userHighlights
|
||||
)
|
||||
|
||||
if (pendingNoteForNewHighlight) {
|
||||
pendingNoteForNewHighlight = false
|
||||
highlightToNoteCfi = finalCfi
|
||||
}
|
||||
},
|
||||
onNoteRequested = { cfi ->
|
||||
if (cfi != null) {
|
||||
highlightToNoteCfi = cfi
|
||||
} else {
|
||||
val highlight = UserHighlight(
|
||||
cfi = cfi,
|
||||
text = text,
|
||||
color = color,
|
||||
chapterIndex = currentChapterIndex
|
||||
)
|
||||
userHighlights.add(highlight)
|
||||
Timber.d("Kotlin: Added new highlight")
|
||||
pendingNoteForNewHighlight = true
|
||||
}
|
||||
},
|
||||
onHighlightDeleted = { cfi ->
|
||||
|
|
@ -2896,13 +2918,24 @@ fun EpubReaderHost(
|
|||
onHighlightCreated = { cfi, text, colorId ->
|
||||
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
processAndAddHighlight(
|
||||
val finalCfi = processAndAddHighlight(
|
||||
newCfi = cfi,
|
||||
newText = text,
|
||||
newColor = color,
|
||||
chapterIndex = currentChapterInPaginatedMode ?: 0,
|
||||
currentList = userHighlights
|
||||
)
|
||||
if (pendingNoteForNewHighlight) {
|
||||
pendingNoteForNewHighlight = false
|
||||
highlightToNoteCfi = finalCfi
|
||||
}
|
||||
},
|
||||
onNoteRequested = { cfi ->
|
||||
if (cfi != null) {
|
||||
highlightToNoteCfi = cfi
|
||||
} else {
|
||||
pendingNoteForNewHighlight = true
|
||||
}
|
||||
},
|
||||
onHighlightDeleted = { cfi ->
|
||||
val toRemove = userHighlights.find { it.cfi == cfi }
|
||||
|
|
@ -3933,6 +3966,60 @@ fun EpubReaderHost(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightToNoteCfi != null) {
|
||||
val targetHighlight = userHighlights.find {
|
||||
it.cfi == highlightToNoteCfi || (highlightToNoteCfi != null && it.cfi.contains(highlightToNoteCfi!!))
|
||||
}
|
||||
if (targetHighlight != null) {
|
||||
AnnotationBottomSheet(
|
||||
highlight = targetHighlight,
|
||||
effectiveBg = effectiveBg,
|
||||
effectiveText = effectiveText,
|
||||
activeHighlightPalette = currentHighlightPalette,
|
||||
onColorChange = { newColor -> onHighlightColorChange(targetHighlight, newColor) },
|
||||
onOpenPaletteManager = { showPaletteManager = true },
|
||||
onDismiss = { highlightToNoteCfi = null },
|
||||
onSave = { noteText ->
|
||||
val index = userHighlights.indexOfFirst { it.cfi == targetHighlight.cfi }
|
||||
if (index != -1) {
|
||||
userHighlights[index] = targetHighlight.copy(note = noteText.takeIf { it.isNotBlank() })
|
||||
}
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onDelete = {
|
||||
userHighlights.remove(targetHighlight)
|
||||
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL && targetHighlight.chapterIndex == currentChapterIndex) {
|
||||
val cssClass = targetHighlight.color.cssClass
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(
|
||||
targetHighlight.cfi)}', '$cssClass');"
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onCopy = {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
val clip = android.content.ClipData.newPlainText("Copied Text", targetHighlight.text)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onDictionary = {
|
||||
onDictionaryLookup(targetHighlight.text)
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onTranslate = {
|
||||
onTranslateLookup(targetHighlight.text)
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onSearch = {
|
||||
onSearchLookup(targetHighlight.text)
|
||||
highlightToNoteCfi = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomTopBanner(bannerMessage = bannerMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -4097,5 +4184,18 @@ fun EpubReaderHost(
|
|||
onCustomThemesUpdated = { customThemes = it; saveCustomThemes(context, it) }
|
||||
)
|
||||
}
|
||||
|
||||
if (showPaletteManager) {
|
||||
PaletteManagerDialog(
|
||||
currentPalette = currentHighlightPalette,
|
||||
onDismiss = { showPaletteManager = false },
|
||||
onSave = { newPalette ->
|
||||
newPalette.forEachIndexed { index, color ->
|
||||
onUpdateHighlightPalette(index, color)
|
||||
}
|
||||
showPaletteManager = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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