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;
|
var cfiToReport = rawCfi;
|
||||||
|
|
||||||
if (rawCfi && rawCfi.includes("|")) {
|
if (rawCfi && rawCfi.includes(";;")) {
|
||||||
var cfiParts = rawCfi.split("|");
|
var cfiParts = rawCfi.split(";;");
|
||||||
cfiToReport = cfiParts[cfiParts.length - 1];
|
cfiToReport = cfiParts[cfiParts.length - 1];
|
||||||
console.log("HandleInteraction: Multi-CFI detected on single span. Reporting top layer: " + cfiToReport);
|
console.log("HandleInteraction: Multi-CFI detected on single span. Reporting top layer: " + cfiToReport);
|
||||||
}
|
}
|
||||||
|
|
@ -2051,31 +2051,30 @@
|
||||||
let idx = parseInt(div.dataset.chunkIndex, 10);
|
let idx = parseInt(div.dataset.chunkIndex, 10);
|
||||||
|
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
// Check if we have data for this chunk at specific index
|
|
||||||
if (!this.chunksData[idx]) {
|
if (!this.chunksData[idx]) {
|
||||||
if (window.ContentBridge && window.ContentBridge.requestChunk) {
|
if (window.ContentBridge && window.ContentBridge.requestChunk) {
|
||||||
window.ContentBridge.requestChunk(idx);
|
window.ContentBridge.requestChunk(idx);
|
||||||
}
|
}
|
||||||
} else if (div.innerHTML === "") {
|
} else if (div.innerHTML === "") {
|
||||||
// Restore content from cache
|
|
||||||
let oldHeight = div.getBoundingClientRect().height;
|
let oldHeight = div.getBoundingClientRect().height;
|
||||||
div.innerHTML = this.chunksData[idx]; // FIX: Access by index
|
div.innerHTML = this.chunksData[idx];
|
||||||
div.style.height = ""; // Allow auto height
|
div.style.height = "";
|
||||||
|
|
||||||
let newHeight = div.getBoundingClientRect().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) {
|
if (div.getBoundingClientRect().top < 0) {
|
||||||
scrollAdjust += (newHeight - oldHeight);
|
scrollAdjust += (newHeight - oldHeight);
|
||||||
}
|
}
|
||||||
|
if (window.CURRENT_HIGHLIGHTS) {
|
||||||
|
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unload content to save memory/DOM weight
|
|
||||||
if (div.innerHTML !== "") {
|
if (div.innerHTML !== "") {
|
||||||
let oldHeight = div.getBoundingClientRect().height;
|
let oldHeight = div.getBoundingClientRect().height;
|
||||||
this.chunkHeights[idx] = oldHeight;
|
this.chunkHeights[idx] = oldHeight;
|
||||||
div.style.height = oldHeight + "px"; // Fix height to placeholder
|
div.style.height = oldHeight + "px";
|
||||||
div.innerHTML = "";
|
div.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2115,6 +2114,9 @@
|
||||||
if (div.getBoundingClientRect().bottom < 0) {
|
if (div.getBoundingClientRect().bottom < 0) {
|
||||||
window.scrollBy(0, newHeight - oldHeight);
|
window.scrollBy(0, newHeight - oldHeight);
|
||||||
}
|
}
|
||||||
|
if (window.CURRENT_HIGHLIGHTS) {
|
||||||
|
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.checkImagesForDiagnosis) {
|
if (window.checkImagesForDiagnosis) {
|
||||||
|
|
@ -2133,7 +2135,7 @@
|
||||||
|
|
||||||
allSpans.forEach((span) => {
|
allSpans.forEach((span) => {
|
||||||
var currentCfiAttr = span.getAttribute("data-cfi") || "";
|
var currentCfiAttr = span.getAttribute("data-cfi") || "";
|
||||||
var cfis = currentCfiAttr.split("|");
|
var cfis = currentCfiAttr.split(";;");
|
||||||
|
|
||||||
if (cfis.includes(cfi)) {
|
if (cfis.includes(cfi)) {
|
||||||
var classesToRemove = [];
|
var classesToRemove = [];
|
||||||
|
|
@ -2288,10 +2290,10 @@
|
||||||
|
|
||||||
if (parent && parent.tagName === "SPAN" && parent.classList.contains(className)) {
|
if (parent && parent.tagName === "SPAN" && parent.classList.contains(className)) {
|
||||||
var currentCfi = parent.getAttribute("data-cfi") || "";
|
var currentCfi = parent.getAttribute("data-cfi") || "";
|
||||||
var cfiList = currentCfi.split("|");
|
var cfiList = currentCfi.split(";;");
|
||||||
|
|
||||||
if (!cfiList.includes(newCfi)) {
|
if (!cfiList.includes(newCfi)) {
|
||||||
parent.setAttribute("data-cfi", currentCfi + "|" + newCfi);
|
parent.setAttribute("data-cfi", currentCfi ? (currentCfi + ";;" + newCfi) : newCfi);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (node.nodeValue.trim().length === 0) return;
|
if (node.nodeValue.trim().length === 0) return;
|
||||||
|
|
@ -2366,7 +2368,7 @@
|
||||||
|
|
||||||
allSpans.forEach((span) => {
|
allSpans.forEach((span) => {
|
||||||
var currentCfiAttr = span.getAttribute("data-cfi") || "";
|
var currentCfiAttr = span.getAttribute("data-cfi") || "";
|
||||||
var cfiList = currentCfiAttr.split("|");
|
var cfiList = currentCfiAttr.split(";;");
|
||||||
|
|
||||||
// Detailed check for match
|
// Detailed check for match
|
||||||
if (cfiList.includes(cfiToRemove)) {
|
if (cfiList.includes(cfiToRemove)) {
|
||||||
|
|
@ -2397,15 +2399,14 @@
|
||||||
parent.normalize();
|
parent.normalize();
|
||||||
removedCount++;
|
removedCount++;
|
||||||
} else {
|
} else {
|
||||||
// CASE 2: Overlapping highlight -> Update data-cfi
|
|
||||||
console.log(`$ {
|
console.log(`$ {
|
||||||
HL_LOG_TAG
|
HL_LOG_TAG
|
||||||
}
|
}
|
||||||
|
|
||||||
: -> Updating span (remaining CFIs: $ {
|
: -> Updating span (remaining CFIs: $ {
|
||||||
newCfiList.join('|')
|
newCfiList.join(';;')
|
||||||
}).`);
|
}).`);
|
||||||
span.setAttribute("data-cfi", newCfiList.join("|"));
|
span.setAttribute("data-cfi", newCfiList.join(";;"));
|
||||||
|
|
||||||
if (optionalCssClass) {
|
if (optionalCssClass) {
|
||||||
console.log(`$ {
|
console.log(`$ {
|
||||||
|
|
@ -2497,9 +2498,16 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
applyHighlight: function (cfi, text, cssClass) {
|
applyHighlight: function (cfi, text, cssClass) {
|
||||||
// "Healed" Apply Logic: Checks text equality before applying
|
|
||||||
try {
|
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);
|
const location = window.getNodeAndOffsetFromCfi(cfi);
|
||||||
if (!location || !location.node) return;
|
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_CUSTOM_THEMES
|
||||||
import com.aryan.reader.epubreader.PREF_READER_THEME
|
import com.aryan.reader.epubreader.PREF_READER_THEME
|
||||||
import com.aryan.reader.paginatedreader.TtsChunk
|
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.GOOGLE_TTS_SPEAKERS
|
||||||
import com.aryan.reader.tts.SpeakerSamplePlayer
|
import com.aryan.reader.tts.SpeakerSamplePlayer
|
||||||
import com.aryan.reader.tts.TtsPlaybackManager
|
import com.aryan.reader.tts.TtsPlaybackManager
|
||||||
|
|
@ -2617,3 +2618,206 @@ fun ColorSlider(color: Color, onColorChanged: (Color) -> Unit) {
|
||||||
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))
|
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.compose.ui.window.PopupPositionProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.aryan.reader.ReaderTexture
|
import com.aryan.reader.ReaderTexture
|
||||||
import com.aryan.reader.paginatedreader.PaginatedTextSelectionMenu
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
@ -280,7 +279,9 @@ private data class CustomMenuState(
|
||||||
val selectionBounds: Rect,
|
val selectionBounds: Rect,
|
||||||
val finishActionModeCallback: () -> Unit,
|
val finishActionModeCallback: () -> Unit,
|
||||||
val cfi: String? = null,
|
val cfi: String? = null,
|
||||||
val isExistingHighlight: Boolean = false
|
val isExistingHighlight: Boolean = false,
|
||||||
|
val note: String? = null,
|
||||||
|
val selectedColor: HighlightColor? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|
@ -344,6 +345,7 @@ fun ChapterWebView(
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
onTranslate: (String) -> Unit,
|
onTranslate: (String) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
onNoteRequested: (String?) -> Unit,
|
||||||
onContentReadyForSummarization: suspend (String) -> Unit,
|
onContentReadyForSummarization: suspend (String) -> Unit,
|
||||||
currentFontFamily: ReaderFont,
|
currentFontFamily: ReaderFont,
|
||||||
customFontPath: String? = null,
|
customFontPath: String? = null,
|
||||||
|
|
@ -368,6 +370,7 @@ fun ChapterWebView(
|
||||||
val jsToInject = remember(context) { getJsToInject(context) }
|
val jsToInject = remember(context) { getJsToInject(context) }
|
||||||
|
|
||||||
var showPaletteManager by remember { mutableStateOf(false) }
|
var showPaletteManager by remember { mutableStateOf(false) }
|
||||||
|
val latestUserHighlights by rememberUpdatedState(userHighlights)
|
||||||
|
|
||||||
val textureBase64 by remember(activeTextureId) {
|
val textureBase64 by remember(activeTextureId) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
|
|
@ -461,7 +464,7 @@ fun ChapterWebView(
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"InteractiveWebView factory for $chapterTitle (Key: $key), isDarkTheme: $isDarkTheme, initialScroll: $initialScrollTarget"
|
"InteractiveWebView factory for $chapterTitle (Key: $key), isDarkTheme: $isDarkTheme, initialScroll: $initialScrollTarget"
|
||||||
)
|
)
|
||||||
val webView = InteractiveWebView(
|
@Suppress("unused") val webView = InteractiveWebView(
|
||||||
context = ctx,
|
context = ctx,
|
||||||
onSingleTap = onTap,
|
onSingleTap = onTap,
|
||||||
onPotentialScroll = onPotentialScroll,
|
onPotentialScroll = onPotentialScroll,
|
||||||
|
|
@ -511,32 +514,11 @@ fun ChapterWebView(
|
||||||
onClickCallback = { cfi, text, left, top, right, bottom ->
|
onClickCallback = { cfi, text, left, top, right, bottom ->
|
||||||
this.post {
|
this.post {
|
||||||
onHighlightClicked()
|
onHighlightClicked()
|
||||||
|
localWebViewRef?.evaluateJavascript(
|
||||||
val densityValue = density.density
|
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||||
val locationOnScreen = IntArray(2)
|
null
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
onNoteRequested(cfi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
), "HighlightBridge"
|
), "HighlightBridge"
|
||||||
|
|
@ -846,7 +828,13 @@ fun ChapterWebView(
|
||||||
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');",
|
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');",
|
||||||
null
|
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
|
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 = {
|
onTts = {
|
||||||
localWebViewRef?.evaluateJavascript(
|
localWebViewRef?.evaluateJavascript(
|
||||||
"javascript:window.TtsBridgeHelper.extractAndRelayTextFromSelection();",
|
"javascript:window.TtsBridgeHelper.extractAndRelayTextFromSelection();",
|
||||||
|
|
@ -1006,21 +1006,13 @@ fun ChapterWebView(
|
||||||
onDelete = if (state.isExistingHighlight && state.cfi != null) {
|
onDelete = if (state.isExistingHighlight && state.cfi != null) {
|
||||||
{
|
{
|
||||||
val highlightToDelete = userHighlights.find { h ->
|
val highlightToDelete = userHighlights.find { h ->
|
||||||
h.cfi == state.cfi || h.cfi.split("|")
|
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
||||||
.contains(state.cfi)
|
|
||||||
}
|
}
|
||||||
if (highlightToDelete != null) {
|
if (highlightToDelete != null) {
|
||||||
val cssClassToDelete = highlightToDelete.color.cssClass
|
val cssClassToDelete = highlightToDelete.color.cssClass
|
||||||
val allCfiParts = highlightToDelete.cfi.split("|")
|
localWebViewRef?.evaluateJavascript(
|
||||||
allCfiParts.forEach { partCfi ->
|
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(highlightToDelete.cfi)}', '$cssClassToDelete');", null
|
||||||
localWebViewRef?.evaluateJavascript(
|
)
|
||||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${
|
|
||||||
escapeJsString(
|
|
||||||
partCfi
|
|
||||||
)
|
|
||||||
}', '$cssClassToDelete');", null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onHighlightDeleted(highlightToDelete.cfi)
|
onHighlightDeleted(highlightToDelete.cfi)
|
||||||
}
|
}
|
||||||
state.finishActionModeCallback()
|
state.finishActionModeCallback()
|
||||||
|
|
@ -1028,7 +1020,10 @@ fun ChapterWebView(
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
isProUser = isProUser,
|
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.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
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.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
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.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||||
import androidx.compose.material.icons.filled.Check
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.aryan.reader.R
|
import com.aryan.reader.R
|
||||||
|
|
@ -96,7 +104,8 @@ data class UserHighlight(
|
||||||
val cfi: String,
|
val cfi: String,
|
||||||
val text: String,
|
val text: String,
|
||||||
val color: HighlightColor,
|
val color: HighlightColor,
|
||||||
val chapterIndex: Int
|
val chapterIndex: Int,
|
||||||
|
val note: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun escapeJsString(value: String): String {
|
fun escapeJsString(value: String): String {
|
||||||
|
|
@ -182,6 +191,7 @@ fun saveHighlightsToPrefs(context: Context, bookTitle: String, highlights: List<
|
||||||
put("text", h.text)
|
put("text", h.text)
|
||||||
put("colorId", h.color.id)
|
put("colorId", h.color.id)
|
||||||
put("chapterIndex", h.chapterIndex)
|
put("chapterIndex", h.chapterIndex)
|
||||||
|
put("note", h.note ?: "")
|
||||||
}
|
}
|
||||||
jsonArray.put(obj)
|
jsonArray.put(obj)
|
||||||
}
|
}
|
||||||
|
|
@ -200,13 +210,15 @@ fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List<UserHighl
|
||||||
val obj = jsonArray.getJSONObject(i)
|
val obj = jsonArray.getJSONObject(i)
|
||||||
val colorId = obj.getString("colorId")
|
val colorId = obj.getString("colorId")
|
||||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||||
|
val noteStr = obj.optString("note", "")
|
||||||
list.add(
|
list.add(
|
||||||
UserHighlight(
|
UserHighlight(
|
||||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||||
cfi = obj.getString("cfi"),
|
cfi = obj.getString("cfi"),
|
||||||
text = obj.getString("text"),
|
text = obj.getString("text"),
|
||||||
color = color,
|
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 obj = jsonArray.getJSONObject(i)
|
||||||
val colorId = obj.getString("colorId")
|
val colorId = obj.getString("colorId")
|
||||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||||
|
val noteStr = obj.optString("note", "")
|
||||||
list.add(
|
list.add(
|
||||||
UserHighlight(
|
UserHighlight(
|
||||||
id = obj.optString("id", java.util.UUID.randomUUID().toString()),
|
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||||
cfi = obj.getString("cfi"),
|
cfi = obj.getString("cfi"),
|
||||||
text = obj.getString("text"),
|
text = obj.getString("text"),
|
||||||
color = color,
|
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("text", h.text)
|
||||||
put("colorId", h.color.id)
|
put("colorId", h.color.id)
|
||||||
put("chapterIndex", h.chapterIndex)
|
put("chapterIndex", h.chapterIndex)
|
||||||
|
put("note", h.note ?: "")
|
||||||
}
|
}
|
||||||
jsonArray.put(obj)
|
jsonArray.put(obj)
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +286,7 @@ fun processAndAddHighlight(
|
||||||
newColor: HighlightColor,
|
newColor: HighlightColor,
|
||||||
chapterIndex: Int,
|
chapterIndex: Int,
|
||||||
currentList: MutableList<UserHighlight>
|
currentList: MutableList<UserHighlight>
|
||||||
) {
|
): String {
|
||||||
val newParts = newCfi.split('|')
|
val newParts = newCfi.split('|')
|
||||||
val newStartFull = newParts.first()
|
val newStartFull = newParts.first()
|
||||||
val newEndFull = newParts.last()
|
val newEndFull = newParts.last()
|
||||||
|
|
@ -286,10 +301,11 @@ fun processAndAddHighlight(
|
||||||
var finalEndPath = newEndPath
|
var finalEndPath = newEndPath
|
||||||
var finalEndOffset = newEndOffset
|
var finalEndOffset = newEndOffset
|
||||||
var finalText = newText
|
var finalText = newText
|
||||||
|
var finalNote: String? = null
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val existing = iterator.next()
|
val existing = iterator.next()
|
||||||
if (existing.chapterIndex != chapterIndex || existing.color != newColor) continue
|
if (existing.chapterIndex != chapterIndex) continue
|
||||||
|
|
||||||
val exParts = existing.cfi.split('|')
|
val exParts = existing.cfi.split('|')
|
||||||
val exStartFull = exParts.first()
|
val exStartFull = exParts.first()
|
||||||
|
|
@ -317,6 +333,7 @@ fun processAndAddHighlight(
|
||||||
|
|
||||||
if (!isDisjoint) {
|
if (!isDisjoint) {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
|
if (existing.note != null && finalNote == null) finalNote = existing.note
|
||||||
val unionStartCmp = comparePaths(finalStartPath, exStartPath)
|
val unionStartCmp = comparePaths(finalStartPath, exStartPath)
|
||||||
if (unionStartCmp > 0 || (unionStartCmp == 0 && finalStartOffset > exStartOffset)) {
|
if (unionStartCmp > 0 || (unionStartCmp == 0 && finalStartOffset > exStartOffset)) {
|
||||||
finalStartPath = exStartPath
|
finalStartPath = exStartPath
|
||||||
|
|
@ -331,12 +348,15 @@ fun processAndAddHighlight(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val finalCfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset"
|
||||||
currentList.add(UserHighlight(
|
currentList.add(UserHighlight(
|
||||||
cfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset",
|
cfi = finalCfi,
|
||||||
text = finalText,
|
text = finalText,
|
||||||
color = newColor,
|
color = newColor,
|
||||||
chapterIndex = chapterIndex
|
chapterIndex = chapterIndex,
|
||||||
|
note = finalNote
|
||||||
))
|
))
|
||||||
|
return finalCfi
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI Components ---
|
// --- UI Components ---
|
||||||
|
|
@ -480,3 +500,458 @@ fun PaletteManagerDialog(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.gestures.rememberDraggableState
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -65,6 +66,7 @@ import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -213,7 +215,11 @@ fun EpubReaderDrawerSheet(
|
||||||
onNavigateToHighlight: (UserHighlight) -> Unit,
|
onNavigateToHighlight: (UserHighlight) -> Unit,
|
||||||
onDeleteBookmark: (Bookmark) -> Unit,
|
onDeleteBookmark: (Bookmark) -> Unit,
|
||||||
onRenameBookmark: (Bookmark, String) -> Unit,
|
onRenameBookmark: (Bookmark, String) -> Unit,
|
||||||
onDeleteHighlight: (UserHighlight) -> Unit
|
onDeleteHighlight: (UserHighlight) -> Unit,
|
||||||
|
onEditNote: (UserHighlight) -> Unit,
|
||||||
|
activeHighlightPalette: List<HighlightColor>,
|
||||||
|
onOpenPaletteManager: () -> Unit,
|
||||||
|
onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit
|
||||||
) {
|
) {
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars)
|
modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars)
|
||||||
|
|
@ -236,7 +242,7 @@ fun EpubReaderDrawerSheet(
|
||||||
Tab(
|
Tab(
|
||||||
selected = drawerPagerState.currentPage == 2,
|
selected = drawerPagerState.currentPage == 2,
|
||||||
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(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,
|
userHighlights = userHighlights,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
onNavigateToHighlight = onNavigateToHighlight,
|
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
|
@Composable
|
||||||
private fun HighlightsList(
|
private fun HighlightsList(
|
||||||
userHighlights: List<UserHighlight>,
|
userHighlights: List<UserHighlight>,
|
||||||
chapters: List<EpubChapter>,
|
chapters: List<EpubChapter>,
|
||||||
onNavigateToHighlight: (UserHighlight) -> Unit,
|
onNavigateToHighlight: (UserHighlight) -> Unit,
|
||||||
onDeleteHighlight: (UserHighlight) -> Unit
|
onDeleteHighlight: (UserHighlight) -> Unit,
|
||||||
|
onEditNote: (UserHighlight) -> Unit,
|
||||||
|
activeHighlightPalette: List<HighlightColor>,
|
||||||
|
onOpenPaletteManager: () -> Unit,
|
||||||
|
onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit
|
||||||
) {
|
) {
|
||||||
if (userHighlights.isEmpty()) {
|
if (userHighlights.isEmpty()) {
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||||
|
|
@ -653,76 +668,138 @@ private fun HighlightsList(
|
||||||
} else {
|
} else {
|
||||||
var highlightMenuExpandedFor by remember { mutableStateOf<UserHighlight?>(null) }
|
var highlightMenuExpandedFor by remember { mutableStateOf<UserHighlight?>(null) }
|
||||||
var showHighlightDeleteDialogFor by remember { mutableStateOf<UserHighlight?>(null) }
|
var showHighlightDeleteDialogFor by remember { mutableStateOf<UserHighlight?>(null) }
|
||||||
|
var filterWithNotesOnly by remember { mutableStateOf(false) } // ADDED
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
Row(
|
||||||
state = listState,
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
modifier = Modifier.fillMaxSize().padding(end = 4.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
androidx.compose.material3.FilterChip(
|
||||||
items = userHighlights.sortedBy { it.chapterIndex },
|
selected = !filterWithNotesOnly,
|
||||||
key = { it.id }
|
onClick = { filterWithNotesOnly = false },
|
||||||
) { highlight ->
|
label = { Text("All") }
|
||||||
val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter)
|
)
|
||||||
|
androidx.compose.material3.FilterChip(
|
||||||
ListItem(
|
selected = filterWithNotesOnly,
|
||||||
headlineContent = {
|
onClick = { filterWithNotesOnly = true },
|
||||||
Text(
|
label = { Text("With Notes") }
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalScrollbar(
|
val filteredHighlights = if (filterWithNotesOnly) {
|
||||||
listState = listState,
|
userHighlights.filter { !it.note.isNullOrBlank() }
|
||||||
modifier = Modifier.align(Alignment.CenterEnd)
|
} 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 ->
|
showHighlightDeleteDialogFor?.let { highlightToDelete ->
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,9 @@ fun EpubReaderHost(
|
||||||
var sliderStartPage by remember { mutableIntStateOf(0) }
|
var sliderStartPage by remember { mutableIntStateOf(0) }
|
||||||
var startPageThumbnail by remember { mutableStateOf<Bitmap?>(null) }
|
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 showJustifyWarningDialog by remember { mutableStateOf(false) }
|
||||||
var isNavigatingByToc by remember { mutableStateOf(false) }
|
var isNavigatingByToc by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -1022,6 +1025,7 @@ fun EpubReaderHost(
|
||||||
var showTtsSettingsSheet by remember { mutableStateOf(false) }
|
var showTtsSettingsSheet by remember { mutableStateOf(false) }
|
||||||
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
||||||
var showThemePanel by remember { mutableStateOf(false) }
|
var showThemePanel by remember { mutableStateOf(false) }
|
||||||
|
var showPaletteManager by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var currentThemeId by remember { mutableStateOf(loadReaderThemeId(context)) }
|
var currentThemeId by remember { mutableStateOf(loadReaderThemeId(context)) }
|
||||||
var customThemes by remember { mutableStateOf(loadCustomThemes(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() {
|
fun startTts() {
|
||||||
if (isAutoScrollModeActive) {
|
if (isAutoScrollModeActive) {
|
||||||
isAutoScrollModeActive = false
|
isAutoScrollModeActive = false
|
||||||
|
|
@ -1637,13 +1653,16 @@ fun EpubReaderHost(
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
EpubReaderDrawerSheet(
|
EpubReaderDrawerSheet(
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
tableOfContents = epubBook.tableOfContents, // Pass TOC
|
tableOfContents = epubBook.tableOfContents,
|
||||||
activeFragmentId = activeFragmentId,
|
activeFragmentId = activeFragmentId,
|
||||||
bookmarks = bookmarks,
|
bookmarks = bookmarks,
|
||||||
userHighlights = userHighlights,
|
userHighlights = userHighlights,
|
||||||
currentChapterIndex = currentChapterIndex,
|
currentChapterIndex = currentChapterIndex,
|
||||||
currentChapterInPaginatedMode = currentChapterInPaginatedMode,
|
currentChapterInPaginatedMode = currentChapterInPaginatedMode,
|
||||||
renderMode = currentRenderMode,
|
renderMode = currentRenderMode,
|
||||||
|
activeHighlightPalette = currentHighlightPalette,
|
||||||
|
onOpenPaletteManager = { showPaletteManager = true },
|
||||||
|
onHighlightColorChange = onHighlightColorChange,
|
||||||
onNavigateToTocEntry = { entry ->
|
onNavigateToTocEntry = { entry ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
drawerState.close()
|
drawerState.close()
|
||||||
|
|
@ -1948,15 +1967,14 @@ fun EpubReaderHost(
|
||||||
highlightToDelete.chapterIndex == currentChapterIndex) {
|
highlightToDelete.chapterIndex == currentChapterIndex) {
|
||||||
|
|
||||||
val cssClass = highlightToDelete.color.cssClass
|
val cssClass = highlightToDelete.color.cssClass
|
||||||
val cfiParts = highlightToDelete.cfi.split("|")
|
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(highlightToDelete.cfi)}', '$cssClass');"
|
||||||
|
Timber.d("Executing JS removal for highlight: ${highlightToDelete.cfi}")
|
||||||
cfiParts.forEach { partCfi ->
|
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClass');"
|
|
||||||
Timber.d("Executing JS removal for part: $partCfi")
|
|
||||||
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): Creating Highlight. CFI: $cfi")
|
||||||
Timber.d("Vertical Mode (Source): Text Snippet: '${text.take(50)}...'")
|
Timber.d("Vertical Mode (Source): Text Snippet: '${text.take(50)}...'")
|
||||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||||
val existingIndex = userHighlights.indexOfFirst { it.cfi == cfi }
|
|
||||||
|
|
||||||
if (existingIndex != -1) {
|
val finalCfi = processAndAddHighlight(
|
||||||
val existing = userHighlights[existingIndex]
|
newCfi = cfi,
|
||||||
userHighlights[existingIndex] = existing.copy(color = color, text = text)
|
newText = text,
|
||||||
Timber.d("Kotlin: Updated existing highlight at index $existingIndex")
|
newColor = color,
|
||||||
|
chapterIndex = currentChapterIndex,
|
||||||
|
currentList = userHighlights
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pendingNoteForNewHighlight) {
|
||||||
|
pendingNoteForNewHighlight = false
|
||||||
|
highlightToNoteCfi = finalCfi
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNoteRequested = { cfi ->
|
||||||
|
if (cfi != null) {
|
||||||
|
highlightToNoteCfi = cfi
|
||||||
} else {
|
} else {
|
||||||
val highlight = UserHighlight(
|
pendingNoteForNewHighlight = true
|
||||||
cfi = cfi,
|
|
||||||
text = text,
|
|
||||||
color = color,
|
|
||||||
chapterIndex = currentChapterIndex
|
|
||||||
)
|
|
||||||
userHighlights.add(highlight)
|
|
||||||
Timber.d("Kotlin: Added new highlight")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHighlightDeleted = { cfi ->
|
onHighlightDeleted = { cfi ->
|
||||||
|
|
@ -2896,13 +2918,24 @@ fun EpubReaderHost(
|
||||||
onHighlightCreated = { cfi, text, colorId ->
|
onHighlightCreated = { cfi, text, colorId ->
|
||||||
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
||||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||||
processAndAddHighlight(
|
val finalCfi = processAndAddHighlight(
|
||||||
newCfi = cfi,
|
newCfi = cfi,
|
||||||
newText = text,
|
newText = text,
|
||||||
newColor = color,
|
newColor = color,
|
||||||
chapterIndex = currentChapterInPaginatedMode ?: 0,
|
chapterIndex = currentChapterInPaginatedMode ?: 0,
|
||||||
currentList = userHighlights
|
currentList = userHighlights
|
||||||
)
|
)
|
||||||
|
if (pendingNoteForNewHighlight) {
|
||||||
|
pendingNoteForNewHighlight = false
|
||||||
|
highlightToNoteCfi = finalCfi
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNoteRequested = { cfi ->
|
||||||
|
if (cfi != null) {
|
||||||
|
highlightToNoteCfi = cfi
|
||||||
|
} else {
|
||||||
|
pendingNoteForNewHighlight = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onHighlightDeleted = { cfi ->
|
onHighlightDeleted = { cfi ->
|
||||||
val toRemove = userHighlights.find { it.cfi == 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)
|
CustomTopBanner(bannerMessage = bannerMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4097,5 +4184,18 @@ fun EpubReaderHost(
|
||||||
onCustomThemesUpdated = { customThemes = it; saveCustomThemes(context, it) }
|
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 android.graphics.RectF
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
|
@ -45,23 +48,40 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
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.CopyAll
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Search
|
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.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.painterResource
|
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.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
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.OcrLine
|
||||||
import com.aryan.reader.pdf.ocr.OcrResult
|
import com.aryan.reader.pdf.ocr.OcrResult
|
||||||
import com.aryan.reader.pdf.ocr.OcrSymbol
|
import com.aryan.reader.pdf.ocr.OcrSymbol
|
||||||
import io.legere.pdfiumandroid.suspend.PdfTextPageKt
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -111,7 +130,8 @@ data class PdfUserHighlight(
|
||||||
val bounds: List<RectF>,
|
val bounds: List<RectF>,
|
||||||
val color: PdfHighlightColor,
|
val color: PdfHighlightColor,
|
||||||
val text: String,
|
val text: String,
|
||||||
val range: Pair<Int, Int>
|
val range: Pair<Int, Int>,
|
||||||
|
val note: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
internal data class CustomPdfMenuState(
|
internal data class CustomPdfMenuState(
|
||||||
|
|
@ -122,7 +142,9 @@ internal data class CustomPdfMenuState(
|
||||||
val highlightId: String? = null,
|
val highlightId: String? = null,
|
||||||
val isComment: Boolean = false,
|
val isComment: Boolean = false,
|
||||||
val author: String? = null,
|
val author: String? = null,
|
||||||
val annotation: EmbeddedAnnotation? = null
|
val annotation: EmbeddedAnnotation? = null,
|
||||||
|
val note: String? = null,
|
||||||
|
val selectedColor: PdfHighlightColor? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
internal enum class PdfSelectionMethod {
|
internal enum class PdfSelectionMethod {
|
||||||
|
|
@ -197,6 +219,8 @@ private fun CommentThread(replies: List<EmbeddedAnnotation>, depth: Int) {
|
||||||
internal fun PdfSelectionMenuPopup(
|
internal fun PdfSelectionMenuPopup(
|
||||||
menuState: CustomPdfMenuState,
|
menuState: CustomPdfMenuState,
|
||||||
popupPositionProvider: PopupPositionProvider,
|
popupPositionProvider: PopupPositionProvider,
|
||||||
|
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||||
|
onPaletteClick: (() -> Unit)? = null,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onCopy: (String) -> Unit,
|
onCopy: (String) -> Unit,
|
||||||
onAiDefine: (String) -> Unit,
|
onAiDefine: (String) -> Unit,
|
||||||
|
|
@ -205,7 +229,8 @@ internal fun PdfSelectionMenuPopup(
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
onColorSelected: (PdfHighlightColor) -> Unit,
|
onColorSelected: (PdfHighlightColor) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onTts: (() -> Unit)? = null
|
onTts: (() -> Unit)? = null,
|
||||||
|
onNote: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Popup(
|
Popup(
|
||||||
popupPositionProvider = popupPositionProvider,
|
popupPositionProvider = popupPositionProvider,
|
||||||
|
|
@ -224,6 +249,48 @@ internal fun PdfSelectionMenuPopup(
|
||||||
modifier = Modifier.widthIn(max = 300.dp)
|
modifier = Modifier.widthIn(max = 300.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = if (menuState.isComment) Modifier.fillMaxWidth() else Modifier.width(IntrinsicSize.Max)) {
|
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) {
|
if (menuState.isComment) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -264,15 +331,30 @@ internal fun PdfSelectionMenuPopup(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
PdfHighlightColor.entries.forEach { colorEnum ->
|
PdfHighlightColor.entries.forEach { colorEnum ->
|
||||||
|
val displayColor = customHighlightColors[colorEnum] ?: colorEnum.color
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(horizontal = 6.dp).size(32.dp)
|
modifier = Modifier.padding(horizontal = 6.dp).size(32.dp)
|
||||||
.background(colorEnum.color, CircleShape).clip(CircleShape)
|
.background(displayColor, CircleShape).clip(CircleShape)
|
||||||
.clickable {
|
.clickable {
|
||||||
Timber.tag("PdfHighlightDebug")
|
Timber.tag("PdfHighlightDebug")
|
||||||
.d("Color box clicked: $colorEnum")
|
.d("Color box clicked: $colorEnum")
|
||||||
onColorSelected(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()
|
HorizontalDivider()
|
||||||
|
|
@ -288,6 +370,11 @@ internal fun PdfSelectionMenuPopup(
|
||||||
actions.add(MenuActionItem(imageVector = Icons.Default.Search, label = "Search", onClick = { onSearch(menuState.selectedText) }))
|
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) {
|
if (!menuState.isExistingHighlight) {
|
||||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = { onSelectAll() }))
|
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = { onSelectAll() }))
|
||||||
}
|
}
|
||||||
|
|
@ -537,3 +624,213 @@ internal fun mergePdfRectsIntoLines(rects: List<RectF>): List<RectF> {
|
||||||
RectF(m[0], m[3], m[2], m[1])
|
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 selectionHighlightColor: Color,
|
||||||
val pageIndex: Int,
|
val pageIndex: Int,
|
||||||
val userHighlightScreenRects: StableHolder<List<Pair<PdfUserHighlight, List<Rect>>>>,
|
val userHighlightScreenRects: StableHolder<List<Pair<PdfUserHighlight, List<Rect>>>>,
|
||||||
|
val customHighlightColors: StableHolder<Map<PdfHighlightColor, Color>>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|
@ -437,8 +438,11 @@ internal fun PdfPageComposable(
|
||||||
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit = { _,_,_,_ -> },
|
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit = { _,_,_,_ -> },
|
||||||
onHighlightUpdate: (String, PdfHighlightColor) -> Unit = { _,_ -> },
|
onHighlightUpdate: (String, PdfHighlightColor) -> Unit = { _,_ -> },
|
||||||
onHighlightDelete: (String) -> Unit = {},
|
onHighlightDelete: (String) -> Unit = {},
|
||||||
|
onNoteRequested: (String?) -> Unit = {},
|
||||||
onTts: (Int, Int) -> Unit = { _, _ -> },
|
onTts: (Int, Int) -> Unit = { _, _ -> },
|
||||||
activeToolThickness: Float = 0f
|
activeToolThickness: Float = 0f,
|
||||||
|
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||||
|
onPaletteClick: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val pdfDocumentItem = pdfDocument.item
|
val pdfDocumentItem = pdfDocument.item
|
||||||
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
||||||
|
|
@ -2552,16 +2556,7 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
if (hitHighlightPair != null && tappedRect != null) {
|
if (hitHighlightPair != null && tappedRect != null) {
|
||||||
val hitHighlight = hitHighlightPair.first
|
val hitHighlight = hitHighlightPair.first
|
||||||
val combinedRect = Rect(hitHighlightPair.second.first())
|
onNoteRequested(hitHighlight.id)
|
||||||
hitHighlightPair.second.forEach { combinedRect.union(it) }
|
|
||||||
|
|
||||||
customMenuState = CustomPdfMenuState(
|
|
||||||
selectedText = hitHighlight.text,
|
|
||||||
anchorRect = combinedRect,
|
|
||||||
charRange = hitHighlight.range,
|
|
||||||
isExistingHighlight = true,
|
|
||||||
highlightId = hitHighlight.id
|
|
||||||
)
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3568,7 +3563,8 @@ internal fun PdfPageComposable(
|
||||||
searchHighlightMode,
|
searchHighlightMode,
|
||||||
searchFocusedColor,
|
searchFocusedColor,
|
||||||
searchAllColor,
|
searchAllColor,
|
||||||
userHighlightScreenRects
|
userHighlightScreenRects,
|
||||||
|
customHighlightColors
|
||||||
) {
|
) {
|
||||||
PageSelectionData(
|
PageSelectionData(
|
||||||
pageLinks = StableHolder(pageLinks),
|
pageLinks = StableHolder(pageLinks),
|
||||||
|
|
@ -3593,6 +3589,7 @@ internal fun PdfPageComposable(
|
||||||
mergedSearchAllRects = StableHolder(mergedSearchAllRects),
|
mergedSearchAllRects = StableHolder(mergedSearchAllRects),
|
||||||
searchHighlightMode = searchHighlightMode,
|
searchHighlightMode = searchHighlightMode,
|
||||||
userHighlightScreenRects = StableHolder(userHighlightScreenRects),
|
userHighlightScreenRects = StableHolder(userHighlightScreenRects),
|
||||||
|
customHighlightColors = StableHolder(customHighlightColors)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3622,6 +3619,7 @@ internal fun PdfPageComposable(
|
||||||
onHighlightUpdate = onHighlightUpdate,
|
onHighlightUpdate = onHighlightUpdate,
|
||||||
onHighlightDelete = onHighlightDelete,
|
onHighlightDelete = onHighlightDelete,
|
||||||
onTts = onTts,
|
onTts = onTts,
|
||||||
|
onNote = onNoteRequested,
|
||||||
teardropHeightPx = teardropHeightPxState.value,
|
teardropHeightPx = teardropHeightPxState.value,
|
||||||
activeDraggingHandle = activeDraggingHandle,
|
activeDraggingHandle = activeDraggingHandle,
|
||||||
showMagnifier = showMagnifier,
|
showMagnifier = showMagnifier,
|
||||||
|
|
@ -3833,7 +3831,9 @@ internal fun PdfPageComposable(
|
||||||
onTextBoxDrag = onTextBoxDrag,
|
onTextBoxDrag = onTextBoxDrag,
|
||||||
onTextBoxDragEnd = onTextBoxDragEnd,
|
onTextBoxDragEnd = onTextBoxDragEnd,
|
||||||
onDragPageTurn = onDragPageTurn,
|
onDragPageTurn = onDragPageTurn,
|
||||||
draggingBoxId = draggingBoxId
|
draggingBoxId = draggingBoxId,
|
||||||
|
customHighlightColors = customHighlightColors,
|
||||||
|
onPaletteClick = onPaletteClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3955,7 +3955,8 @@ private fun PdfHighlightsLayer(
|
||||||
scrimColorForTextHighlight: Color,
|
scrimColorForTextHighlight: Color,
|
||||||
allTextPageHighlightColor: Color,
|
allTextPageHighlightColor: Color,
|
||||||
ttsHighlightColor: Color,
|
ttsHighlightColor: Color,
|
||||||
selectionHighlightColor: Color
|
selectionHighlightColor: Color,
|
||||||
|
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap()
|
||||||
) {
|
) {
|
||||||
Timber.d("PdfHighlightsLayer Recompose")
|
Timber.d("PdfHighlightsLayer Recompose")
|
||||||
Canvas(modifier = Modifier
|
Canvas(modifier = Modifier
|
||||||
|
|
@ -4098,10 +4099,11 @@ private fun PdfHighlightsLayer(
|
||||||
|
|
||||||
// 9. Persistent User Highlights
|
// 9. Persistent User Highlights
|
||||||
userHighlightScreenRects.forEach { (highlight, screenRects) ->
|
userHighlightScreenRects.forEach { (highlight, screenRects) ->
|
||||||
|
val displayColor = customHighlightColors[highlight.color] ?: highlight.color.color
|
||||||
screenRects.forEach { r ->
|
screenRects.forEach { r ->
|
||||||
if (isVisible(r)) {
|
if (isVisible(r)) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = highlight.color.color.copy(alpha = 0.4f),
|
color = displayColor.copy(alpha = 0.4f),
|
||||||
topLeft = Offset(r.left.toFloat(), r.top.toFloat()),
|
topLeft = Offset(r.left.toFloat(), r.top.toFloat()),
|
||||||
size = Size(r.width().toFloat(), r.height().toFloat())
|
size = Size(r.width().toFloat(), r.height().toFloat())
|
||||||
)
|
)
|
||||||
|
|
@ -4471,7 +4473,8 @@ private fun PdfPageSelectionsLayer(
|
||||||
scrimColorForTextHighlight: Color,
|
scrimColorForTextHighlight: Color,
|
||||||
allTextPageHighlightColor: Color,
|
allTextPageHighlightColor: Color,
|
||||||
ttsHighlightColor: Color,
|
ttsHighlightColor: Color,
|
||||||
selectionHighlightColor: Color
|
selectionHighlightColor: Color,
|
||||||
|
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap()
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
Timber.tag("PdfDrawPerf").v("SELECTIONS LAYER: Recomposing")
|
Timber.tag("PdfDrawPerf").v("SELECTIONS LAYER: Recomposing")
|
||||||
|
|
@ -4498,7 +4501,8 @@ private fun PdfPageSelectionsLayer(
|
||||||
scrimColorForTextHighlight = scrimColorForTextHighlight,
|
scrimColorForTextHighlight = scrimColorForTextHighlight,
|
||||||
allTextPageHighlightColor = allTextPageHighlightColor,
|
allTextPageHighlightColor = allTextPageHighlightColor,
|
||||||
ttsHighlightColor = ttsHighlightColor,
|
ttsHighlightColor = ttsHighlightColor,
|
||||||
selectionHighlightColor = selectionHighlightColor
|
selectionHighlightColor = selectionHighlightColor,
|
||||||
|
customHighlightColors = customHighlightColors
|
||||||
)
|
)
|
||||||
|
|
||||||
val highlightTime = (System.nanoTime() - highlightStart) / 1_000_000f
|
val highlightTime = (System.nanoTime() - highlightStart) / 1_000_000f
|
||||||
|
|
@ -4561,12 +4565,14 @@ private fun PdfPageRenderer(
|
||||||
onTextBoxDragEnd: () -> Unit,
|
onTextBoxDragEnd: () -> Unit,
|
||||||
onDragPageTurn: (Int) -> Unit,
|
onDragPageTurn: (Int) -> Unit,
|
||||||
draggingBoxId: String? = null,
|
draggingBoxId: String? = null,
|
||||||
|
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||||
|
onPaletteClick: (() -> Unit)? = null,
|
||||||
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit,
|
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit,
|
||||||
onHighlightUpdate: (String, PdfHighlightColor) -> Unit,
|
onHighlightUpdate: (String, PdfHighlightColor) -> Unit,
|
||||||
onHighlightDelete: (String) -> Unit,
|
onHighlightDelete: (String) -> Unit,
|
||||||
onTts: (Int, Int) -> Unit,
|
onTts: (Int, Int) -> Unit,
|
||||||
activeToolThickness: Float
|
activeToolThickness: Float,
|
||||||
|
onNote: (String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -4604,7 +4610,8 @@ private fun PdfPageRenderer(
|
||||||
scrimColorForTextHighlight = selectionData.scrimColorForTextHighlight,
|
scrimColorForTextHighlight = selectionData.scrimColorForTextHighlight,
|
||||||
allTextPageHighlightColor = selectionData.allTextPageHighlightColor,
|
allTextPageHighlightColor = selectionData.allTextPageHighlightColor,
|
||||||
ttsHighlightColor = selectionData.ttsHighlightColor,
|
ttsHighlightColor = selectionData.ttsHighlightColor,
|
||||||
selectionHighlightColor = selectionData.selectionHighlightColor
|
selectionHighlightColor = selectionData.selectionHighlightColor,
|
||||||
|
customHighlightColors = selectionData.customHighlightColors.item
|
||||||
)
|
)
|
||||||
|
|
||||||
// Layer 3: Annotations & Text
|
// Layer 3: Annotations & Text
|
||||||
|
|
@ -4950,7 +4957,21 @@ private fun PdfPageRenderer(
|
||||||
onTts = {
|
onTts = {
|
||||||
onTts(selectionData.pageIndex, menuState.charRange.first)
|
onTts(selectionData.pageIndex, menuState.charRange.first)
|
||||||
onMenuDismiss()
|
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 = { _,_,_,_ -> },
|
onHighlightAdd: (Int, Pair<Int, Int>, String, PdfHighlightColor) -> Unit = { _,_,_,_ -> },
|
||||||
onHighlightUpdate: (String, PdfHighlightColor) -> Unit = { _,_ -> },
|
onHighlightUpdate: (String, PdfHighlightColor) -> Unit = { _,_ -> },
|
||||||
onHighlightDelete: (String) -> Unit = {},
|
onHighlightDelete: (String) -> Unit = {},
|
||||||
|
onNoteRequested: (String?) -> Unit = {},
|
||||||
onTts: (Int, Int) -> 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.") }
|
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
|
||||||
DisposableEffect(state) {
|
DisposableEffect(state) {
|
||||||
|
|
@ -1576,8 +1579,11 @@ internal fun PdfVerticalReader(
|
||||||
onHighlightAdd = onHighlightAdd,
|
onHighlightAdd = onHighlightAdd,
|
||||||
onHighlightUpdate = onHighlightUpdate,
|
onHighlightUpdate = onHighlightUpdate,
|
||||||
onHighlightDelete = onHighlightDelete,
|
onHighlightDelete = onHighlightDelete,
|
||||||
|
onNoteRequested = onNoteRequested,
|
||||||
onTts = onTts,
|
onTts = onTts,
|
||||||
activeToolThickness = activeToolThickness,
|
activeToolThickness = activeToolThickness,
|
||||||
|
customHighlightColors = customHighlightColors,
|
||||||
|
onPaletteClick = onPaletteClick,
|
||||||
onTextBoxDragStart = { box, localTopLeft, touchOffset ->
|
onTextBoxDragStart = { box, localTopLeft, touchOffset ->
|
||||||
val currentZoom = zoomAnimatable.value
|
val currentZoom = zoomAnimatable.value
|
||||||
val panX = panXAnimatable.value
|
val panX = panXAnimatable.value
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ package com.aryan.reader.pdf
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
|
@ -43,6 +44,7 @@ import android.print.PrintDocumentInfo
|
||||||
import android.print.PrintManager
|
import android.print.PrintManager
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.LruCache
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
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.Brush
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.Edit
|
||||||
import androidx.compose.material.icons.filled.Fullscreen
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
import androidx.compose.material.icons.filled.FullscreenExit
|
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.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
|
@ -153,6 +155,7 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MenuDefaults
|
import androidx.compose.material3.MenuDefaults
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
|
@ -167,6 +170,7 @@ import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -262,6 +266,7 @@ import com.aryan.reader.AiDefinitionResult
|
||||||
import com.aryan.reader.BuildConfig
|
import com.aryan.reader.BuildConfig
|
||||||
import com.aryan.reader.DeviceVoiceSettingsSheet
|
import com.aryan.reader.DeviceVoiceSettingsSheet
|
||||||
import com.aryan.reader.FileType
|
import com.aryan.reader.FileType
|
||||||
|
import com.aryan.reader.HighlightColorPickerDialog
|
||||||
import com.aryan.reader.MainViewModel
|
import com.aryan.reader.MainViewModel
|
||||||
import com.aryan.reader.R
|
import com.aryan.reader.R
|
||||||
import com.aryan.reader.ReaderTheme
|
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_THEME_KEY = "pdf_reader_theme"
|
||||||
private const val PDF_KEEP_SCREEN_ON_KEY = "pdf_keep_screen_on_enabled"
|
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) {
|
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
|
||||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
prefs.edit { putBoolean(PDF_KEEP_SCREEN_ON_KEY, isEnabled) }
|
prefs.edit { putBoolean(PDF_KEEP_SCREEN_ON_KEY, isEnabled) }
|
||||||
|
|
@ -1091,7 +1114,7 @@ private data class DocumentCacheItem(
|
||||||
)
|
)
|
||||||
|
|
||||||
private class DocumentCache(val maxSize: Int = 3) {
|
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(
|
override fun entryRemoved(
|
||||||
evicted: Boolean,
|
evicted: Boolean,
|
||||||
key: String,
|
key: String,
|
||||||
|
|
@ -1189,7 +1212,7 @@ fun PdfViewerScreen(
|
||||||
val effectiveFileType = uiState.selectedFileType ?: FileType.PDF
|
val effectiveFileType = uiState.selectedFileType ?: FileType.PDF
|
||||||
|
|
||||||
var showNewTabSheet by remember { mutableStateOf(false) }
|
var showNewTabSheet by remember { mutableStateOf(false) }
|
||||||
val sheetState = androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||||
|
|
||||||
val isTabsEnabled = uiState.isTabsEnabled
|
val isTabsEnabled = uiState.isTabsEnabled
|
||||||
val openTabs = uiState.openTabs
|
val openTabs = uiState.openTabs
|
||||||
|
|
@ -1280,6 +1303,16 @@ fun PdfViewerScreen(
|
||||||
var isEditMode by rememberSaveable { mutableStateOf(false) }
|
var isEditMode by rememberSaveable { mutableStateOf(false) }
|
||||||
var isDockMinimized 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) {
|
val isDrawingActive by remember(isEditMode, isDockMinimized) {
|
||||||
derivedStateOf { isEditMode && !isDockMinimized }
|
derivedStateOf { isEditMode && !isDockMinimized }
|
||||||
}
|
}
|
||||||
|
|
@ -1394,6 +1427,10 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
val (initialDockLocation, initialDockOffset) = remember(context) { loadDockState(context) }
|
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 dockLocation by remember { mutableStateOf(initialDockLocation) }
|
||||||
var dockOffset by remember { mutableStateOf(initialDockOffset) }
|
var dockOffset by remember { mutableStateOf(initialDockOffset) }
|
||||||
var snapPreviewLocation by remember { mutableStateOf<DockLocation?>(null) }
|
var snapPreviewLocation by remember { mutableStateOf<DockLocation?>(null) }
|
||||||
|
|
@ -1834,6 +1871,10 @@ fun PdfViewerScreen(
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
userHighlights.add(newHighlight)
|
userHighlights.add(newHighlight)
|
||||||
Timber.tag("PdfExportDebug").d("userHighlights now contains ${userHighlights.size} items.")
|
Timber.tag("PdfExportDebug").d("userHighlights now contains ${userHighlights.size} items.")
|
||||||
|
if (pendingNoteForNewHighlight) {
|
||||||
|
pendingNoteForNewHighlight = false
|
||||||
|
highlightToNoteId = newHighlight.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4094,61 +4135,125 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var showDeleteConfirmDialogFor by remember {
|
var showDeleteConfirmDialogFor by remember { mutableStateOf<PdfUserHighlight?>(null) }
|
||||||
mutableStateOf<PdfUserHighlight?>(null)
|
var filterWithNotesOnly by remember { mutableStateOf(false) }
|
||||||
}
|
|
||||||
val sortedHighlights = remember(userHighlights.toList()) {
|
|
||||||
userHighlights.sortedBy { it.pageIndex }
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
itemsIndexed(
|
Row(
|
||||||
items = sortedHighlights,
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
key = { _, highlight -> highlight.id }
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) { _, highlight ->
|
) {
|
||||||
ListItem(
|
FilterChip(
|
||||||
headlineContent = {
|
selected = !filterWithNotesOnly,
|
||||||
Text(
|
onClick = { filterWithNotesOnly = false },
|
||||||
text = highlight.text.ifBlank { "Highlighted section" },
|
label = { Text("All") }
|
||||||
maxLines = 2,
|
)
|
||||||
overflow = TextOverflow.Ellipsis,
|
FilterChip(
|
||||||
modifier = Modifier
|
selected = filterWithNotesOnly,
|
||||||
.background(
|
onClick = { filterWithNotesOnly = true },
|
||||||
color = highlight.color.color.copy(alpha = 0.3f),
|
label = { Text("With Notes") }
|
||||||
shape = RoundedCornerShape(4.dp)
|
)
|
||||||
)
|
}
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
|
||||||
)
|
val filteredHighlights = if (filterWithNotesOnly) {
|
||||||
},
|
userHighlights.filter { !it.note.isNullOrBlank() }
|
||||||
supportingContent = {
|
} else {
|
||||||
Text(
|
userHighlights.toList()
|
||||||
"Page ${highlight.pageIndex + 1}",
|
}
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
val sortedHighlights = remember(filteredHighlights) {
|
||||||
},
|
filteredHighlights.sortedBy { it.pageIndex }
|
||||||
trailingContent = {
|
}
|
||||||
IconButton(
|
|
||||||
onClick = { showDeleteConfirmDialogFor = highlight }
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
) {
|
itemsIndexed(
|
||||||
Icon(
|
items = sortedHighlights,
|
||||||
imageVector = Icons.Default.Delete,
|
key = { _, highlight -> highlight.id }
|
||||||
contentDescription = "Delete highlight",
|
) { _, highlight ->
|
||||||
tint = MaterialTheme.colorScheme.error
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = highlight.text.ifBlank { "Highlighted section" },
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
},
|
supportingContent = {
|
||||||
modifier = Modifier.clickable {
|
Column {
|
||||||
coroutineScope.launch {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
drawerState.close()
|
val displayColor = customHighlightColors[highlight.color] ?: highlight.color.color
|
||||||
if (displayMode == DisplayMode.PAGINATION) {
|
|
||||||
pagerState.scrollToPage(highlight.pageIndex)
|
Box(
|
||||||
} else {
|
modifier = Modifier
|
||||||
verticalReaderState.scrollToPage(highlight.pageIndex)
|
.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,
|
totalPages = totalDisplayPages,
|
||||||
activeTheme = activeTheme,
|
activeTheme = activeTheme,
|
||||||
isScrollLocked = isScrollLocked,
|
isScrollLocked = isScrollLocked,
|
||||||
|
customHighlightColors = customHighlightColors,
|
||||||
|
onPaletteClick = {
|
||||||
|
highlightColorPickerInitialSlot = PdfHighlightColor.YELLOW
|
||||||
|
showHighlightColorPicker = true
|
||||||
|
},
|
||||||
onScaleChanged = { newScale ->
|
onScaleChanged = { newScale ->
|
||||||
if (pagerState.currentPage == pageIndex) {
|
if (pagerState.currentPage == pageIndex) {
|
||||||
currentPageScale = newScale
|
currentPageScale = newScale
|
||||||
|
|
@ -4506,6 +4616,7 @@ fun PdfViewerScreen(
|
||||||
onHighlightAdd = onHighlightAdd,
|
onHighlightAdd = onHighlightAdd,
|
||||||
onHighlightUpdate = onHighlightUpdate,
|
onHighlightUpdate = onHighlightUpdate,
|
||||||
onHighlightDelete = onHighlightDelete,
|
onHighlightDelete = onHighlightDelete,
|
||||||
|
onNoteRequested = onNoteRequested,
|
||||||
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
||||||
activeToolThickness = currentStrokeWidthState,
|
activeToolThickness = currentStrokeWidthState,
|
||||||
onTwoFingerSwipe = { direction ->
|
onTwoFingerSwipe = { direction ->
|
||||||
|
|
@ -4817,6 +4928,8 @@ fun PdfViewerScreen(
|
||||||
pdfDocument = docHolder,
|
pdfDocument = docHolder,
|
||||||
activeTheme = activeTheme,
|
activeTheme = activeTheme,
|
||||||
isScrollLocked = isScrollLocked,
|
isScrollLocked = isScrollLocked,
|
||||||
|
customHighlightColors = customHighlightColors,
|
||||||
|
onPaletteClick = { showHighlightColorPicker = true },
|
||||||
totalPages = totalDisplayPages,
|
totalPages = totalDisplayPages,
|
||||||
pageAspectRatios = ratiosHolder,
|
pageAspectRatios = ratiosHolder,
|
||||||
virtualPages = virtualPages,
|
virtualPages = virtualPages,
|
||||||
|
|
@ -4841,6 +4954,7 @@ fun PdfViewerScreen(
|
||||||
onHighlightAdd = onHighlightAdd,
|
onHighlightAdd = onHighlightAdd,
|
||||||
onHighlightUpdate = onHighlightUpdate,
|
onHighlightUpdate = onHighlightUpdate,
|
||||||
onHighlightDelete = onHighlightDelete,
|
onHighlightDelete = onHighlightDelete,
|
||||||
|
onNoteRequested = onNoteRequested,
|
||||||
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
onTts = { pageIdx, charIdx -> startTtsWithPermissionCheck(pageIdx, charIdx) },
|
||||||
activeToolThickness = currentStrokeWidthState,
|
activeToolThickness = currentStrokeWidthState,
|
||||||
onLinkClicked = onLinkClickedStable,
|
onLinkClicked = onLinkClickedStable,
|
||||||
|
|
@ -6999,7 +7113,7 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showNewTabSheet) {
|
if (showNewTabSheet) {
|
||||||
androidx.compose.material3.ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showNewTabSheet = false },
|
onDismissRequest = { showNewTabSheet = false },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
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) {
|
if (showThemePanel) {
|
||||||
ReaderThemePanel(
|
ReaderThemePanel(
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,10 @@ object HighlightSerializer {
|
||||||
obj.put("rangeStart", h.range.first)
|
obj.put("rangeStart", h.range.first)
|
||||||
obj.put("rangeEnd", h.range.second)
|
obj.put("rangeEnd", h.range.second)
|
||||||
|
|
||||||
|
if (!h.note.isNullOrBlank()) {
|
||||||
|
obj.put("note", h.note)
|
||||||
|
}
|
||||||
|
|
||||||
val boundsArray = JSONArray()
|
val boundsArray = JSONArray()
|
||||||
h.bounds.forEach { r ->
|
h.bounds.forEach { r ->
|
||||||
val rObj = JSONObject()
|
val rObj = JSONObject()
|
||||||
|
|
@ -264,7 +268,8 @@ object HighlightSerializer {
|
||||||
bounds = bounds,
|
bounds = bounds,
|
||||||
color = try { PdfHighlightColor.valueOf(obj.getString("color")) } catch(_: Exception) { PdfHighlightColor.YELLOW },
|
color = try { PdfHighlightColor.valueOf(obj.getString("color")) } catch(_: Exception) { PdfHighlightColor.YELLOW },
|
||||||
text = obj.optString("text", ""),
|
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