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:
Aryan 2026-04-05 09:52:05 +05:30 committed by GitHub
parent c8f361376f
commit 65e0570d0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2026 additions and 744 deletions

View file

@ -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
}
)
}
}
}