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
|
|
@ -82,7 +82,6 @@ import androidx.compose.ui.window.Popup
|
|||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.aryan.reader.ReaderTexture
|
||||
import com.aryan.reader.paginatedreader.PaginatedTextSelectionMenu
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
|
@ -280,7 +279,9 @@ private data class CustomMenuState(
|
|||
val selectionBounds: Rect,
|
||||
val finishActionModeCallback: () -> Unit,
|
||||
val cfi: String? = null,
|
||||
val isExistingHighlight: Boolean = false
|
||||
val isExistingHighlight: Boolean = false,
|
||||
val note: String? = null,
|
||||
val selectedColor: HighlightColor? = null
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
@ -344,6 +345,7 @@ fun ChapterWebView(
|
|||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onNoteRequested: (String?) -> Unit,
|
||||
onContentReadyForSummarization: suspend (String) -> Unit,
|
||||
currentFontFamily: ReaderFont,
|
||||
customFontPath: String? = null,
|
||||
|
|
@ -368,6 +370,7 @@ fun ChapterWebView(
|
|||
val jsToInject = remember(context) { getJsToInject(context) }
|
||||
|
||||
var showPaletteManager by remember { mutableStateOf(false) }
|
||||
val latestUserHighlights by rememberUpdatedState(userHighlights)
|
||||
|
||||
val textureBase64 by remember(activeTextureId) {
|
||||
mutableStateOf(
|
||||
|
|
@ -461,7 +464,7 @@ fun ChapterWebView(
|
|||
Timber.d(
|
||||
"InteractiveWebView factory for $chapterTitle (Key: $key), isDarkTheme: $isDarkTheme, initialScroll: $initialScrollTarget"
|
||||
)
|
||||
val webView = InteractiveWebView(
|
||||
@Suppress("unused") val webView = InteractiveWebView(
|
||||
context = ctx,
|
||||
onSingleTap = onTap,
|
||||
onPotentialScroll = onPotentialScroll,
|
||||
|
|
@ -511,32 +514,11 @@ fun ChapterWebView(
|
|||
onClickCallback = { cfi, text, left, top, right, bottom ->
|
||||
this.post {
|
||||
onHighlightClicked()
|
||||
|
||||
val densityValue = density.density
|
||||
val locationOnScreen = IntArray(2)
|
||||
this.getLocationOnScreen(locationOnScreen)
|
||||
val xOffset = locationOnScreen[0]
|
||||
val yOffset = locationOnScreen[1]
|
||||
|
||||
val rect = Rect(
|
||||
(left * densityValue).toInt() + xOffset,
|
||||
(top * densityValue).toInt() + yOffset,
|
||||
(right * densityValue).toInt() + xOffset,
|
||||
(bottom * densityValue).toInt() + yOffset
|
||||
)
|
||||
|
||||
customMenuState = CustomMenuState(
|
||||
selectedText = text,
|
||||
selectionBounds = rect,
|
||||
finishActionModeCallback = {
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||
null
|
||||
)
|
||||
},
|
||||
cfi = cfi,
|
||||
isExistingHighlight = true
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||
null
|
||||
)
|
||||
onNoteRequested(cfi)
|
||||
}
|
||||
}
|
||||
), "HighlightBridge"
|
||||
|
|
@ -846,7 +828,13 @@ fun ChapterWebView(
|
|||
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');",
|
||||
null
|
||||
)
|
||||
}, modifier = Modifier.fillMaxSize()
|
||||
|
||||
val escapedHighlights = escapeJsString(highlightsJson)
|
||||
webView.evaluateJavascript(
|
||||
"javascript:window.CURRENT_HIGHLIGHTS = '${escapedHighlights}'; window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);",
|
||||
null
|
||||
)
|
||||
}, modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +977,19 @@ fun ChapterWebView(
|
|||
}
|
||||
customMenuState = null
|
||||
},
|
||||
onHighlight = null, // Highlight handles itself above in the Colors Row
|
||||
onNote = {
|
||||
if (state.isExistingHighlight && state.cfi != null) {
|
||||
onNoteRequested(state.cfi)
|
||||
} else {
|
||||
onNoteRequested(null)
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.createUserHighlight('${HighlightColor.YELLOW.cssClass}', '${HighlightColor.YELLOW.id}');", null
|
||||
)
|
||||
}
|
||||
state.finishActionModeCallback()
|
||||
customMenuState = null
|
||||
},
|
||||
onHighlight = null,
|
||||
onTts = {
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.TtsBridgeHelper.extractAndRelayTextFromSelection();",
|
||||
|
|
@ -1006,21 +1006,13 @@ fun ChapterWebView(
|
|||
onDelete = if (state.isExistingHighlight && state.cfi != null) {
|
||||
{
|
||||
val highlightToDelete = userHighlights.find { h ->
|
||||
h.cfi == state.cfi || h.cfi.split("|")
|
||||
.contains(state.cfi)
|
||||
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
||||
}
|
||||
if (highlightToDelete != null) {
|
||||
val cssClassToDelete = highlightToDelete.color.cssClass
|
||||
val allCfiParts = highlightToDelete.cfi.split("|")
|
||||
allCfiParts.forEach { partCfi ->
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${
|
||||
escapeJsString(
|
||||
partCfi
|
||||
)
|
||||
}', '$cssClassToDelete');", null
|
||||
)
|
||||
}
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(highlightToDelete.cfi)}', '$cssClassToDelete');", null
|
||||
)
|
||||
onHighlightDeleted(highlightToDelete.cfi)
|
||||
}
|
||||
state.finishActionModeCallback()
|
||||
|
|
@ -1028,7 +1020,10 @@ fun ChapterWebView(
|
|||
}
|
||||
} else null,
|
||||
isProUser = isProUser,
|
||||
isOss = isOss)
|
||||
isOss = isOss,
|
||||
existingNote = state.note,
|
||||
selectedColor = state.selectedColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import timber.log.Timber
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -44,15 +45,22 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import com.aryan.reader.R
|
||||
|
|
@ -96,7 +104,8 @@ data class UserHighlight(
|
|||
val cfi: String,
|
||||
val text: String,
|
||||
val color: HighlightColor,
|
||||
val chapterIndex: Int
|
||||
val chapterIndex: Int,
|
||||
val note: String? = null
|
||||
)
|
||||
|
||||
fun escapeJsString(value: String): String {
|
||||
|
|
@ -182,6 +191,7 @@ fun saveHighlightsToPrefs(context: Context, bookTitle: String, highlights: List<
|
|||
put("text", h.text)
|
||||
put("colorId", h.color.id)
|
||||
put("chapterIndex", h.chapterIndex)
|
||||
put("note", h.note ?: "")
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
}
|
||||
|
|
@ -200,13 +210,15 @@ fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List<UserHighl
|
|||
val obj = jsonArray.getJSONObject(i)
|
||||
val colorId = obj.getString("colorId")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val noteStr = obj.optString("note", "")
|
||||
list.add(
|
||||
UserHighlight(
|
||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||
cfi = obj.getString("cfi"),
|
||||
text = obj.getString("text"),
|
||||
color = color,
|
||||
chapterIndex = obj.getInt("chapterIndex")
|
||||
chapterIndex = obj.getInt("chapterIndex"),
|
||||
note = noteStr.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -225,13 +237,15 @@ fun parseHighlightsJson(jsonString: String?): List<UserHighlight> {
|
|||
val obj = jsonArray.getJSONObject(i)
|
||||
val colorId = obj.getString("colorId")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val noteStr = obj.optString("note", "")
|
||||
list.add(
|
||||
UserHighlight(
|
||||
id = obj.optString("id", java.util.UUID.randomUUID().toString()),
|
||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||
cfi = obj.getString("cfi"),
|
||||
text = obj.getString("text"),
|
||||
color = color,
|
||||
chapterIndex = obj.getInt("chapterIndex")
|
||||
chapterIndex = obj.getInt("chapterIndex"),
|
||||
note = noteStr.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -250,6 +264,7 @@ fun highlightsToJson(highlights: List<UserHighlight>): String {
|
|||
put("text", h.text)
|
||||
put("colorId", h.color.id)
|
||||
put("chapterIndex", h.chapterIndex)
|
||||
put("note", h.note ?: "")
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
}
|
||||
|
|
@ -271,7 +286,7 @@ fun processAndAddHighlight(
|
|||
newColor: HighlightColor,
|
||||
chapterIndex: Int,
|
||||
currentList: MutableList<UserHighlight>
|
||||
) {
|
||||
): String {
|
||||
val newParts = newCfi.split('|')
|
||||
val newStartFull = newParts.first()
|
||||
val newEndFull = newParts.last()
|
||||
|
|
@ -286,10 +301,11 @@ fun processAndAddHighlight(
|
|||
var finalEndPath = newEndPath
|
||||
var finalEndOffset = newEndOffset
|
||||
var finalText = newText
|
||||
var finalNote: String? = null
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val existing = iterator.next()
|
||||
if (existing.chapterIndex != chapterIndex || existing.color != newColor) continue
|
||||
if (existing.chapterIndex != chapterIndex) continue
|
||||
|
||||
val exParts = existing.cfi.split('|')
|
||||
val exStartFull = exParts.first()
|
||||
|
|
@ -317,6 +333,7 @@ fun processAndAddHighlight(
|
|||
|
||||
if (!isDisjoint) {
|
||||
iterator.remove()
|
||||
if (existing.note != null && finalNote == null) finalNote = existing.note
|
||||
val unionStartCmp = comparePaths(finalStartPath, exStartPath)
|
||||
if (unionStartCmp > 0 || (unionStartCmp == 0 && finalStartOffset > exStartOffset)) {
|
||||
finalStartPath = exStartPath
|
||||
|
|
@ -331,12 +348,15 @@ fun processAndAddHighlight(
|
|||
}
|
||||
}
|
||||
|
||||
val finalCfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset"
|
||||
currentList.add(UserHighlight(
|
||||
cfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset",
|
||||
cfi = finalCfi,
|
||||
text = finalText,
|
||||
color = newColor,
|
||||
chapterIndex = chapterIndex
|
||||
chapterIndex = chapterIndex,
|
||||
note = finalNote
|
||||
))
|
||||
return finalCfi
|
||||
}
|
||||
|
||||
// --- UI Components ---
|
||||
|
|
@ -479,4 +499,459 @@ fun PaletteManagerDialog(
|
|||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnnotationBottomSheet(
|
||||
highlight: UserHighlight,
|
||||
effectiveBg: Color,
|
||||
effectiveText: Color,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
onColorChange: (HighlightColor) -> Unit,
|
||||
onOpenPaletteManager: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var noteText by remember { mutableStateOf(highlight.note ?: "") }
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = effectiveBg, // Matches user theme
|
||||
contentColor = effectiveText,
|
||||
contentWindowInsets = { WindowInsets.navigationBars }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
// Top: Highlight Colors
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = highlight.color,
|
||||
onColorSelect = onColorChange,
|
||||
onOpenPaletteManager = onOpenPaletteManager,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// Middle: Elegant Highlight Snippet Card
|
||||
Surface(
|
||||
color = highlight.color.color.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(1.dp, highlight.color.color.copy(alpha = 0.3f)),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
// Left colored accent bar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(6.dp)
|
||||
.fillMaxHeight()
|
||||
.background(highlight.color.color)
|
||||
)
|
||||
Text(
|
||||
text = "\"${highlight.text}\"",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic),
|
||||
maxLines = 4,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
color = effectiveText.copy(alpha = 0.9f),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Action Tools Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
BottomSheetToolButton(icon = R.drawable.copy, label = "Copy", onClick = onCopy, effectiveText = effectiveText)
|
||||
BottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", onClick = onDictionary, effectiveText = effectiveText)
|
||||
BottomSheetToolButton(icon = R.drawable.translate, label = "Translate", onClick = onTranslate, effectiveText = effectiveText)
|
||||
BottomSheetToolButton(icon = R.drawable.search, label = "Search", onClick = onSearch, effectiveText = effectiveText)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Note TextField
|
||||
OutlinedTextField(
|
||||
value = noteText,
|
||||
onValueChange = { noteText = it },
|
||||
placeholder = { Text("Add a note...", color = effectiveText.copy(alpha = 0.5f)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 100.dp),
|
||||
maxLines = 5,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = effectiveText.copy(alpha = 0.3f),
|
||||
focusedTextColor = effectiveText,
|
||||
unfocusedTextColor = effectiveText
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Bottom Actions
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onDelete,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Delete")
|
||||
}
|
||||
Button(
|
||||
onClick = { onSave(noteText) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
) {
|
||||
Text("Save Note")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetToolButton(
|
||||
icon: Int,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
effectiveText: Color
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = label,
|
||||
tint = effectiveText.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = effectiveText.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaginatedTextSelectionMenu(
|
||||
onCopy: () -> Unit,
|
||||
onSelectAll: (() -> Unit)?,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
onHighlight: ((HighlightColor) -> Unit)?,
|
||||
onNote: (() -> Unit)? = null,
|
||||
onDelete: (() -> Unit)?,
|
||||
onTts: (() -> Unit)?,
|
||||
@Suppress("unused") isProUser: Boolean,
|
||||
@Suppress("unused") isOss: Boolean,
|
||||
activeHighlightPalette: List<HighlightColor> = emptyList(),
|
||||
onOpenPaletteManager: (() -> Unit)? = null
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shadowElevation = 6.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 200.dp)) {
|
||||
if (onHighlight != null) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = null,
|
||||
onColorSelect = onHighlight,
|
||||
onOpenPaletteManager = onOpenPaletteManager
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
val actions = mutableListOf<MenuActionItem>()
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = onCopy))
|
||||
if (onTts != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts))
|
||||
}
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = onDictionary))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch))
|
||||
|
||||
if (onNote != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = "Note", onClick = onNote))
|
||||
}
|
||||
|
||||
if (onSelectAll != null) {
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = onSelectAll))
|
||||
}
|
||||
if (onDelete != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = onDelete, isError = true))
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(bottom = 4.dp)) {
|
||||
actions.chunked(3).forEach { rowActions ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
rowActions.forEach { action ->
|
||||
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { action.onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (action.imageVector != null) {
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
} else if (action.iconRes != null) {
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
|
||||
}
|
||||
}
|
||||
repeat(3 - rowActions.size) {
|
||||
Spacer(modifier = Modifier.width(64.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MenuActionItem(
|
||||
val iconRes: Int? = null,
|
||||
val imageVector: androidx.compose.ui.graphics.vector.ImageVector? = null,
|
||||
val label: String,
|
||||
val onClick: () -> Unit,
|
||||
val isError: Boolean = false
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun HighlightColorRow(
|
||||
modifier: Modifier = Modifier,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
selectedColor: HighlightColor? = null,
|
||||
onColorSelect: (HighlightColor) -> Unit,
|
||||
onOpenPaletteManager: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
activeHighlightPalette.forEach { colorEnum ->
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape) // 1. Clip shape for ripple
|
||||
.background(colorEnum.color) // 2. Apply background
|
||||
.clickable {
|
||||
Timber.d("HighlightColorRow: Color clicked -> ${colorEnum.name}")
|
||||
onColorSelect(colorEnum)
|
||||
} // 3. Add clickable (ripple)
|
||||
.border( // 4. Add border on top
|
||||
width = if (selectedColor == colorEnum) 3.dp else 1.dp,
|
||||
color = if (selectedColor == colorEnum) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline.copy(alpha=0.3f),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
if (selectedColor == colorEnum) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = if (colorEnum == HighlightColor.WHITE || colorEnum == HighlightColor.YELLOW) Color.Black else Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onOpenPaletteManager != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
SpectrumButton(
|
||||
onClick = onOpenPaletteManager,
|
||||
size = 32.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaginatedTextSelectionMenu(
|
||||
onCopy: () -> Unit,
|
||||
onSelectAll: (() -> Unit)?,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
onHighlight: ((HighlightColor) -> Unit)?,
|
||||
onNote: (() -> Unit)? = null,
|
||||
onDelete: (() -> Unit)?,
|
||||
onTts: (() -> Unit)?,
|
||||
@Suppress("unused") isProUser: Boolean,
|
||||
@Suppress("unused") isOss: Boolean,
|
||||
activeHighlightPalette: List<HighlightColor> = emptyList(),
|
||||
onOpenPaletteManager: (() -> Unit)? = null,
|
||||
existingNote: String? = null,
|
||||
selectedColor: HighlightColor? = null
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shadowElevation = 6.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 200.dp)) {
|
||||
// 1. Colors Row
|
||||
if (onHighlight != null) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = selectedColor,
|
||||
onColorSelect = onHighlight,
|
||||
onOpenPaletteManager = onOpenPaletteManager
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
// 2. Improved Comment/Note View
|
||||
if (!existingNote.isNullOrBlank()) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 140.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Note",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
"Note",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = existingNote,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
val actions = mutableListOf<MenuActionItem>()
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = onCopy))
|
||||
if (onTts != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts))
|
||||
}
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = onDictionary))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate))
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch))
|
||||
|
||||
if (onNote != null) {
|
||||
val noteLabel = if (existingNote.isNullOrBlank()) "Note" else "Edit"
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
|
||||
}
|
||||
|
||||
if (onSelectAll != null) {
|
||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = onSelectAll))
|
||||
}
|
||||
if (onDelete != null) {
|
||||
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = onDelete, isError = true))
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(bottom = 4.dp)) {
|
||||
actions.chunked(3).forEach { rowActions ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
rowActions.forEach { action ->
|
||||
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.clickable { action.onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (action.imageVector != null) {
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
} else if (action.iconRes != null) {
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
|
||||
}
|
||||
}
|
||||
repeat(3 - rowActions.size) {
|
||||
Spacer(modifier = Modifier.width(64.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import androidx.compose.foundation.gestures.draggable
|
|||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -65,6 +66,7 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -213,7 +215,11 @@ fun EpubReaderDrawerSheet(
|
|||
onNavigateToHighlight: (UserHighlight) -> Unit,
|
||||
onDeleteBookmark: (Bookmark) -> Unit,
|
||||
onRenameBookmark: (Bookmark, String) -> Unit,
|
||||
onDeleteHighlight: (UserHighlight) -> Unit
|
||||
onDeleteHighlight: (UserHighlight) -> Unit,
|
||||
onEditNote: (UserHighlight) -> Unit,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
onOpenPaletteManager: () -> Unit,
|
||||
onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit
|
||||
) {
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars)
|
||||
|
|
@ -236,7 +242,7 @@ fun EpubReaderDrawerSheet(
|
|||
Tab(
|
||||
selected = drawerPagerState.currentPage == 2,
|
||||
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } },
|
||||
text = { Text(stringResource(R.string.tab_highlights)) }
|
||||
text = { Text("Annotations") }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +273,11 @@ fun EpubReaderDrawerSheet(
|
|||
userHighlights = userHighlights,
|
||||
chapters = chapters,
|
||||
onNavigateToHighlight = onNavigateToHighlight,
|
||||
onDeleteHighlight = onDeleteHighlight
|
||||
onDeleteHighlight = onDeleteHighlight,
|
||||
onEditNote = onEditNote,
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
onOpenPaletteManager = onOpenPaletteManager,
|
||||
onHighlightColorChange = onHighlightColorChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -639,12 +649,17 @@ private fun BookmarksList(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun HighlightsList(
|
||||
userHighlights: List<UserHighlight>,
|
||||
chapters: List<EpubChapter>,
|
||||
onNavigateToHighlight: (UserHighlight) -> Unit,
|
||||
onDeleteHighlight: (UserHighlight) -> Unit
|
||||
onDeleteHighlight: (UserHighlight) -> Unit,
|
||||
onEditNote: (UserHighlight) -> Unit,
|
||||
activeHighlightPalette: List<HighlightColor>,
|
||||
onOpenPaletteManager: () -> Unit,
|
||||
onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit
|
||||
) {
|
||||
if (userHighlights.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
|
|
@ -653,76 +668,138 @@ private fun HighlightsList(
|
|||
} else {
|
||||
var highlightMenuExpandedFor by remember { mutableStateOf<UserHighlight?>(null) }
|
||||
var showHighlightDeleteDialogFor by remember { mutableStateOf<UserHighlight?>(null) }
|
||||
var filterWithNotesOnly by remember { mutableStateOf(false) } // ADDED
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize().padding(end = 4.dp)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
items = userHighlights.sortedBy { it.chapterIndex },
|
||||
key = { it.id }
|
||||
) { highlight ->
|
||||
val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = highlight.text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(highlight.color.color, CircleShape)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = chapterTitle,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Box {
|
||||
IconButton(onClick = { highlightMenuExpandedFor = highlight }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.content_desc_options)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = highlightMenuExpandedFor == highlight,
|
||||
onDismissRequest = { highlightMenuExpandedFor = null }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.action_delete)) },
|
||||
onClick = {
|
||||
showHighlightDeleteDialogFor = highlight
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable { onNavigateToHighlight(highlight) }
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
androidx.compose.material3.FilterChip(
|
||||
selected = !filterWithNotesOnly,
|
||||
onClick = { filterWithNotesOnly = false },
|
||||
label = { Text("All") }
|
||||
)
|
||||
androidx.compose.material3.FilterChip(
|
||||
selected = filterWithNotesOnly,
|
||||
onClick = { filterWithNotesOnly = true },
|
||||
label = { Text("With Notes") }
|
||||
)
|
||||
}
|
||||
|
||||
VerticalScrollbar(
|
||||
listState = listState,
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
)
|
||||
val filteredHighlights = if (filterWithNotesOnly) {
|
||||
userHighlights.filter { !it.note.isNullOrBlank() }
|
||||
} else {
|
||||
userHighlights
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize().padding(end = 4.dp)
|
||||
) {
|
||||
items(
|
||||
items = filteredHighlights.sortedBy { it.chapterIndex },
|
||||
key = { it.id }
|
||||
) { highlight ->
|
||||
val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = highlight.text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(highlight.color.color, CircleShape)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = chapterTitle,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (!highlight.note.isNullOrBlank()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = highlight.note,
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Box {
|
||||
IconButton(onClick = { highlightMenuExpandedFor = highlight }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.content_desc_options)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = highlightMenuExpandedFor == highlight,
|
||||
onDismissRequest = { highlightMenuExpandedFor = null }
|
||||
) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
selectedColor = highlight.color,
|
||||
onColorSelect = { color ->
|
||||
onHighlightColorChange(highlight, color)
|
||||
highlightMenuExpandedFor = null
|
||||
},
|
||||
onOpenPaletteManager = {
|
||||
onOpenPaletteManager()
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (highlight.note.isNullOrBlank()) "Add Note" else "Edit Note") },
|
||||
onClick = {
|
||||
onEditNote(highlight)
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.action_delete)) },
|
||||
onClick = {
|
||||
showHighlightDeleteDialogFor = highlight
|
||||
highlightMenuExpandedFor = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable { onNavigateToHighlight(highlight) }
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollbar(
|
||||
listState = listState,
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
showHighlightDeleteDialogFor?.let { highlightToDelete ->
|
||||
|
|
|
|||
|
|
@ -477,6 +477,9 @@ fun EpubReaderHost(
|
|||
var sliderStartPage by remember { mutableIntStateOf(0) }
|
||||
var startPageThumbnail by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
var pendingNoteForNewHighlight by remember { mutableStateOf(false) }
|
||||
var highlightToNoteCfi by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var showJustifyWarningDialog by remember { mutableStateOf(false) }
|
||||
var isNavigatingByToc by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -1022,6 +1025,7 @@ fun EpubReaderHost(
|
|||
var showTtsSettingsSheet by remember { mutableStateOf(false) }
|
||||
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
||||
var showThemePanel by remember { mutableStateOf(false) }
|
||||
var showPaletteManager by remember { mutableStateOf(false) }
|
||||
|
||||
var currentThemeId by remember { mutableStateOf(loadReaderThemeId(context)) }
|
||||
var customThemes by remember { mutableStateOf(loadCustomThemes(context)) }
|
||||
|
|
@ -1066,6 +1070,18 @@ fun EpubReaderHost(
|
|||
}
|
||||
}
|
||||
|
||||
val onHighlightColorChange: (UserHighlight, HighlightColor) -> Unit = { targetHighlight, newColor ->
|
||||
val index = userHighlights.indexOfFirst { it.cfi == targetHighlight.cfi }
|
||||
if (index != -1) {
|
||||
userHighlights[index] = targetHighlight.copy(color = newColor)
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL && targetHighlight.chapterIndex == currentChapterIndex) {
|
||||
val cssClass = newColor.cssClass
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.updateHighlightStyle('${escapeJsString(targetHighlight.cfi)}', '$cssClass', '${newColor.id}');"
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startTts() {
|
||||
if (isAutoScrollModeActive) {
|
||||
isAutoScrollModeActive = false
|
||||
|
|
@ -1637,13 +1653,16 @@ fun EpubReaderHost(
|
|||
drawerContent = {
|
||||
EpubReaderDrawerSheet(
|
||||
chapters = chapters,
|
||||
tableOfContents = epubBook.tableOfContents, // Pass TOC
|
||||
tableOfContents = epubBook.tableOfContents,
|
||||
activeFragmentId = activeFragmentId,
|
||||
bookmarks = bookmarks,
|
||||
userHighlights = userHighlights,
|
||||
currentChapterIndex = currentChapterIndex,
|
||||
currentChapterInPaginatedMode = currentChapterInPaginatedMode,
|
||||
renderMode = currentRenderMode,
|
||||
activeHighlightPalette = currentHighlightPalette,
|
||||
onOpenPaletteManager = { showPaletteManager = true },
|
||||
onHighlightColorChange = onHighlightColorChange,
|
||||
onNavigateToTocEntry = { entry ->
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
|
|
@ -1948,15 +1967,14 @@ fun EpubReaderHost(
|
|||
highlightToDelete.chapterIndex == currentChapterIndex) {
|
||||
|
||||
val cssClass = highlightToDelete.color.cssClass
|
||||
val cfiParts = highlightToDelete.cfi.split("|")
|
||||
|
||||
cfiParts.forEach { partCfi ->
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClass');"
|
||||
Timber.d("Executing JS removal for part: $partCfi")
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(highlightToDelete.cfi)}', '$cssClass');"
|
||||
Timber.d("Executing JS removal for highlight: ${highlightToDelete.cfi}")
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
}
|
||||
},
|
||||
onEditNote = { highlight ->
|
||||
highlightToNoteCfi = highlight.cfi
|
||||
},
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
|
@ -2238,21 +2256,25 @@ fun EpubReaderHost(
|
|||
Timber.d("Vertical Mode (Source): Creating Highlight. CFI: $cfi")
|
||||
Timber.d("Vertical Mode (Source): Text Snippet: '${text.take(50)}...'")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val existingIndex = userHighlights.indexOfFirst { it.cfi == cfi }
|
||||
|
||||
if (existingIndex != -1) {
|
||||
val existing = userHighlights[existingIndex]
|
||||
userHighlights[existingIndex] = existing.copy(color = color, text = text)
|
||||
Timber.d("Kotlin: Updated existing highlight at index $existingIndex")
|
||||
val finalCfi = processAndAddHighlight(
|
||||
newCfi = cfi,
|
||||
newText = text,
|
||||
newColor = color,
|
||||
chapterIndex = currentChapterIndex,
|
||||
currentList = userHighlights
|
||||
)
|
||||
|
||||
if (pendingNoteForNewHighlight) {
|
||||
pendingNoteForNewHighlight = false
|
||||
highlightToNoteCfi = finalCfi
|
||||
}
|
||||
},
|
||||
onNoteRequested = { cfi ->
|
||||
if (cfi != null) {
|
||||
highlightToNoteCfi = cfi
|
||||
} else {
|
||||
val highlight = UserHighlight(
|
||||
cfi = cfi,
|
||||
text = text,
|
||||
color = color,
|
||||
chapterIndex = currentChapterIndex
|
||||
)
|
||||
userHighlights.add(highlight)
|
||||
Timber.d("Kotlin: Added new highlight")
|
||||
pendingNoteForNewHighlight = true
|
||||
}
|
||||
},
|
||||
onHighlightDeleted = { cfi ->
|
||||
|
|
@ -2896,13 +2918,24 @@ fun EpubReaderHost(
|
|||
onHighlightCreated = { cfi, text, colorId ->
|
||||
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
processAndAddHighlight(
|
||||
val finalCfi = processAndAddHighlight(
|
||||
newCfi = cfi,
|
||||
newText = text,
|
||||
newColor = color,
|
||||
chapterIndex = currentChapterInPaginatedMode ?: 0,
|
||||
currentList = userHighlights
|
||||
)
|
||||
if (pendingNoteForNewHighlight) {
|
||||
pendingNoteForNewHighlight = false
|
||||
highlightToNoteCfi = finalCfi
|
||||
}
|
||||
},
|
||||
onNoteRequested = { cfi ->
|
||||
if (cfi != null) {
|
||||
highlightToNoteCfi = cfi
|
||||
} else {
|
||||
pendingNoteForNewHighlight = true
|
||||
}
|
||||
},
|
||||
onHighlightDeleted = { cfi ->
|
||||
val toRemove = userHighlights.find { it.cfi == cfi }
|
||||
|
|
@ -3933,6 +3966,60 @@ fun EpubReaderHost(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightToNoteCfi != null) {
|
||||
val targetHighlight = userHighlights.find {
|
||||
it.cfi == highlightToNoteCfi || (highlightToNoteCfi != null && it.cfi.contains(highlightToNoteCfi!!))
|
||||
}
|
||||
if (targetHighlight != null) {
|
||||
AnnotationBottomSheet(
|
||||
highlight = targetHighlight,
|
||||
effectiveBg = effectiveBg,
|
||||
effectiveText = effectiveText,
|
||||
activeHighlightPalette = currentHighlightPalette,
|
||||
onColorChange = { newColor -> onHighlightColorChange(targetHighlight, newColor) },
|
||||
onOpenPaletteManager = { showPaletteManager = true },
|
||||
onDismiss = { highlightToNoteCfi = null },
|
||||
onSave = { noteText ->
|
||||
val index = userHighlights.indexOfFirst { it.cfi == targetHighlight.cfi }
|
||||
if (index != -1) {
|
||||
userHighlights[index] = targetHighlight.copy(note = noteText.takeIf { it.isNotBlank() })
|
||||
}
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onDelete = {
|
||||
userHighlights.remove(targetHighlight)
|
||||
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL && targetHighlight.chapterIndex == currentChapterIndex) {
|
||||
val cssClass = targetHighlight.color.cssClass
|
||||
val jsCommand = "javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(
|
||||
targetHighlight.cfi)}', '$cssClass');"
|
||||
webViewRefForTts?.evaluateJavascript(jsCommand, null)
|
||||
}
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onCopy = {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
val clip = android.content.ClipData.newPlainText("Copied Text", targetHighlight.text)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onDictionary = {
|
||||
onDictionaryLookup(targetHighlight.text)
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onTranslate = {
|
||||
onTranslateLookup(targetHighlight.text)
|
||||
highlightToNoteCfi = null
|
||||
},
|
||||
onSearch = {
|
||||
onSearchLookup(targetHighlight.text)
|
||||
highlightToNoteCfi = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomTopBanner(bannerMessage = bannerMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -4097,5 +4184,18 @@ fun EpubReaderHost(
|
|||
onCustomThemesUpdated = { customThemes = it; saveCustomThemes(context, it) }
|
||||
)
|
||||
}
|
||||
|
||||
if (showPaletteManager) {
|
||||
PaletteManagerDialog(
|
||||
currentPalette = currentHighlightPalette,
|
||||
onDismiss = { showPaletteManager = false },
|
||||
onSave = { newPalette ->
|
||||
newPalette.forEachIndexed { index, color ->
|
||||
onUpdateHighlightPalette(index, color)
|
||||
}
|
||||
showPaletteManager = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue