Pdf text highlight (#49)

* feat(pdf): implement native text highlighting and interactive selection menu

- Add `PdfUserHighlight` data model to track page index, character ranges, and normalized coordinates.
- Redesign `PdfSelectionMenuPopup` to match the EPUB reader style, featuring a color palette row and a deletion option.
- Integrate highlight rendering into the `PdfPageSelectionsLayer` using Canvas drawing.
- Add hit-testing to `detectTapGestures` to allow users to tap existing highlights to change colors or remove them.

* Implemented PDF highlight persistence, sync, and exporting.

* Improved PDF highlight exporting and visibility logic

- Fixed PDF highlight rendering by correctly calculating coordinates and merging adjacent rectangles into lines.
- Updated `PdfExporter` to include highlights in the exported document and added debug logging.
- Corrected visibility check in `PdfPageComposable` to account for vertical offsets.
- Enhanced `PdfViewerScreen` highlight creation to use merged line rectangles

* Refactored annotation synchronization logic and improved background sync reliability.

- Refactored JSON bundle creation and extraction into reusable helper functions.
- Improved stale file detection by checking all annotation types (ink, text, layout, highlights, text boxes).
- Added background annotation downloading to `SyncWorker`
- Implemented duplicate file cleanup in `GoogleDriveRepository` during uploads.
- Added a `showFeedback` flag to `syncFolderMetadata` to control UI visibility during background operations.
- Fixed a potential data loss edge case by aborting Firestore sync if the bundle upload fails.

* Improved highlight interaction and management in PDF viewer

- Implemented hit tolerance for highlight selection to improve touch detection accuracy.
- Added logic to shift highlight page indices when adding or deleting pages.
- Included highlights in the auto-pruning check for empty pages.
- Added a "Highlights" tab to the navigation drawer to list, navigate to, and delete user highlights.
This commit is contained in:
Aryan 2026-03-09 17:37:44 +05:30 committed by GitHub
parent 8c7c8cb48e
commit ef1bfdc57a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 921 additions and 163 deletions

View file

@ -52,6 +52,7 @@ class RecentFilesRepository(private val context: Context) {
private val pdfRichTextRepository = PdfRichTextRepository(context)
private val pageLayoutRepository = PageLayoutRepository(context)
private val pdfTextBoxRepository = PdfTextBoxRepository(context)
private val pdfHighlightRepository = com.aryan.reader.pdf.data.PdfHighlightRepository(context)
init {
if (!coverCacheDir.exists()) {
@ -87,6 +88,7 @@ class RecentFilesRepository(private val context: Context) {
coverCacheDir.deleteRecursively()
}
coverCacheDir.mkdirs()
pdfHighlightRepository.clearAll()
Timber.d("Cleared all local book data and cover cache.")
}
@ -182,15 +184,17 @@ class RecentFilesRepository(private val context: Context) {
val richTextFile = pdfRichTextRepository.getFileForSync(bookId)
val layoutFile = pageLayoutRepository.getLayoutFile(bookId)
val textBoxFile = pdfTextBoxRepository.getFileForSync(bookId)
val highlightFile = pdfHighlightRepository.getFileForSync(bookId)
val hasInk = inkFile?.exists() == true
val hasRichText = richTextFile.exists()
val hasLayout = layoutFile.exists()
val hasTextBoxes = textBoxFile.exists()
val hasHighlights = highlightFile.exists()
Timber.tag("FolderAnnotationSync").d("File checks -> hasInk: $hasInk, hasRichText: $hasRichText, hasLayout: $hasLayout, hasTextBoxes: $hasTextBoxes")
Timber.tag("FolderAnnotationSync").d("File checks -> hasInk: $hasInk, hasRichText: $hasRichText, hasLayout: $hasLayout, hasTextBoxes: $hasTextBoxes, hasHighlights: $hasHighlights")
if (!hasInk && !hasRichText && !hasLayout && !hasTextBoxes) {
if (!hasInk && !hasRichText && !hasLayout && !hasTextBoxes && !hasHighlights) {
Timber.tag("FolderAnnotationSync").d("No annotations found locally for bookId: $bookId. Aborting sync.")
return@withContext
}
@ -214,13 +218,15 @@ class RecentFilesRepository(private val context: Context) {
if (hasRichText) putJsonSafe("text", richTextFile)
if (hasLayout) putJsonSafe("layout", layoutFile)
if (hasTextBoxes) putJsonSafe("textBoxes", textBoxFile)
if (hasHighlights) putJsonSafe("highlights", highlightFile)
val tsInk = if(hasInk) inkFile.lastModified() else 0L
val tsText = if(hasRichText) richTextFile.lastModified() else 0L
val tsLayout = if(hasLayout) layoutFile.lastModified() else 0L
val tsBox = if(hasTextBoxes) textBoxFile.lastModified() else 0L
val tsHighlight = if(hasHighlights) highlightFile.lastModified() else 0L
val maxFileTs = maxOf(tsInk, tsText, tsLayout, tsBox)
val maxFileTs = maxOf(tsInk, tsText, tsLayout, tsBox, tsHighlight)
val finalTs = maxOf(maxFileTs, System.currentTimeMillis())
Timber.tag("FolderAnnotationSync").d("Pushing annotation bundle for $bookId to folder. finalTs=$finalTs")
@ -263,6 +269,9 @@ class RecentFilesRepository(private val context: Context) {
// 4. Text Boxes
writeSafe("textBoxes", pdfTextBoxRepository.getFileForSync(bookId))
// 5. Highlights
writeSafe("highlights", pdfHighlightRepository.getFileForSync(bookId))
Timber.tag("FolderAnnotationSync").i("Successfully imported annotation bundle for $bookId from folder.")
} catch (e: Exception) {
Timber.tag("FolderAnnotationSync").e(e, "Failed to import annotation bundle for $bookId")
@ -294,10 +303,6 @@ class RecentFilesRepository(private val context: Context) {
return@withContext recentFileDao.getFolderBooksWithoutCovers().map { it.toRecentFileItem() }
}
suspend fun updateReflowPreference(bookId: String, isPreferred: Boolean) = withContext(Dispatchers.IO) {
recentFileDao.updateReflowPreference(bookId, isPreferred)
}
suspend fun detachAllFolderBooks() = withContext(Dispatchers.IO) {
recentFileDao.detachAllFolderBooks()
Timber.d("Detached all folder books. They are now standard local files.")