book-reader/desktopApp/src/desktopMain/kotlin/com/aryan/reader/desktop/DesktopCloudSidecarSync.kt
Aryan 9510293ac3
Update v1.0.49 (#330)
* Added PDF top tab strip visibility toggle and fixed WebView hit test NPE

* Refactored desktop reader screens and state management into specialized components

* Added image gallery to reader sidebar and refactored desktop PDF UI components

* Implemented EPUB image gallery

* Refactored reader and library models to use shared common types, centralizing file type resolution and texture management while removing redundant mapping logic

* Standardized UI styling and refactored app navigation layout

* Implemented auto-hiding reader chrome and activity tracking in desktop

* Refactored reader panels into distinct left and right modal layers with platform-specific sizing and keyboard navigation support.

* Added PPTX support for desktop and refactored parsing into a shared module

* Implement paid AI features and account management for the desktop application.

* Implement AI Hub and enhanced Cloud TTS integration for Desktop

* Implement streaming support for AI definition and summarization features

* Implement support for password-protected PDFs and file actions in the desktop reader.

* Implement cloud synchronization for desktop using Firestore and Google Drive

* Implement PDF reflow and "Text View" for the desktop reader

* Refactor OPDS logic to use SharedOpdsController

* Optimize PDF tile rendering performance

* Implement two-page spread support for PDF pagination

* Implement two-page spread support for the PDF viewer

* Improved shared spread zoom in PDF viewer

* Improve PDF spread navigation with fling support and configurable page gaps

* Add brightness control to PDF and EPUB readers

* Refactor folder synchronization to use shared logic engine

* Implement safe string formatting and validation for localized resources

* Implement TTS chunk skip navigation

* Implement deep-linking and playback controls for TTS media sessions

* Implement start index for TTS playback

* Improve TTS navigation, prefetching, and notification duration reporting

* Implement TTS mini playback bar for background reading

* Implement multi-window reader support for the desktop application

* Improve desktop modal window management and visibility syncing

* Implement localized string support for Desktop and shared UI

* Implement language selection and persistence for Desktop

* Implement plural string support for Desktop and migrate hardcoded counts to plurals.xml

* Implement localized banner messages and UI strings using resource-backed SharedText

* Implement compact badge styling for small book covers

* Refactor PDF native interaction and improve HTML import memory safety

* fix language persistence

* Refactor reader overflow menus to use section-based logic

* Refactor PDF layout remapping and improve text box interaction

* Improve CFI resolution and TTS resume accuracy using dynamic chunk offsets

* Centralize PDF annotation export mapping and improve metadata handling

* Add support for threaded comments in PDF highlight annotations

* Flatten highlight comments into a single thread for PDF export and allow author editing

* Integrate page slider into reader chrome and persist toggle state

* Handle fragments and queries in EPUB chapter paths

* Implement dynamic, theme-aware coloring for the reader slider

* Implement customizable app-wide font preference

* Implement one-hand zoom gestures in the PDF viewer

* Implement File Information dialog for PDF and EPUB readers

* Bump version to 1.0.49 (53)

* Refactor PDF reader logic into modular components

* Add ProGuard rules to prevent R8 optimization issues in EPUB reader screens

* Add option to use PDF filenames as display names

* Fix preservation of PDF filename display preference in library projection
2026-05-20 22:14:01 +05:30

138 lines
5.9 KiB
Kotlin

package com.aryan.reader.desktop
import com.aryan.reader.shared.BookItem
import com.aryan.reader.shared.FileType
import com.aryan.reader.shared.pdf.SharedPdfAnnotationSerializer
import com.aryan.reader.shared.pdf.SharedPdfAnnotationSidecarCodec
import com.aryan.reader.shared.pdf.SharedPdfRichTextSerializer
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.io.File
internal object DesktopCloudSidecarSync {
fun hasLocalAnnotationData(book: BookItem): Boolean {
val path = book.path?.takeIf { it.isNotBlank() } ?: return false
if (book.type != FileType.PDF) return false
return desktopPdfAnnotationFile(path).isFile ||
desktopPdfBookmarkFile(path).isFile ||
desktopPdfRichTextFile(path).isFile
}
fun localAnnotationTimestamp(book: BookItem): Long {
val path = book.path?.takeIf { it.isNotBlank() } ?: return 0L
if (book.type != FileType.PDF) return 0L
return maxOf(
desktopPdfAnnotationFile(path).lastModifiedIfFile(),
desktopPdfBookmarkFile(path).lastModifiedIfFile(),
desktopPdfRichTextFile(path).lastModifiedIfFile()
)
}
fun exportAnnotationBundle(book: BookItem): File? {
val path = book.path?.takeIf { it.isNotBlank() } ?: return null
if (book.type != FileType.PDF) return null
val annotationFile = desktopPdfAnnotationFile(path)
val bookmarkFile = desktopPdfBookmarkFile(path)
val richTextFile = desktopPdfRichTextFile(path)
val data = buildMap {
if (annotationFile.isFile) {
val annotations = SharedPdfAnnotationSerializer.decode(annotationFile.readText())
put(
SharedPdfAnnotationSidecarCodec.KEY_PDF_ANNOTATIONS,
SharedPdfAnnotationSidecarCodec.encodeAnnotationsElement(annotations)
)
}
if (bookmarkFile.isFile) {
cloudSidecarJson.parseElementOrNull(bookmarkFile.readText())?.let { put("bookmarks", it) }
}
if (richTextFile.isFile) {
cloudSidecarJson.parseElementOrNull(richTextFile.readText())?.let { element ->
put("text", SharedPdfRichTextSerializer.encodeElement(SharedPdfRichTextSerializer.decodeElement(element)))
}
}
}
if (data.isEmpty()) return null
val payload = JsonObject(mapOf("version" to JsonPrimitive(2)) + data)
val canonical = SharedPdfAnnotationSidecarCodec.canonicalizeDataJson(
cloudSidecarJson.encodeToString(JsonElement.serializer(), payload)
)
val tempFile = File(
desktopUserCacheRoot(),
"sync_bundle_${book.id.toDesktopSafeFileName()}_${System.nanoTime()}.json"
)
tempFile.parentFile?.mkdirs()
tempFile.writeText(canonical)
return tempFile
}
fun importAnnotationBundle(book: BookItem, rawJson: String, timestamp: Long): Boolean {
val path = book.path?.takeIf { it.isNotBlank() } ?: return false
if (book.type != FileType.PDF) return false
val root = cloudSidecarJson.parseElementOrNull(rawJson)?.jsonObjectOrNull() ?: return false
val data = root["data"]?.jsonObjectOrNull() ?: root
val canonicalData = SharedPdfAnnotationSidecarCodec.withCanonicalAnnotations(data)
val annotationFile = desktopPdfAnnotationFile(path)
val bookmarkFile = desktopPdfBookmarkFile(path)
val richTextFile = desktopPdfRichTextFile(path)
if (canonicalData.hasPdfAnnotationPayload()) {
val annotations = SharedPdfAnnotationSidecarCodec.annotationsFromData(canonicalData)
annotationFile.parentFile?.mkdirs()
annotationFile.writeText(SharedPdfAnnotationSerializer.encode(annotations))
annotationFile.setLastModified(timestamp)
} else if (annotationFile.isFile) {
annotationFile.delete()
}
canonicalData["bookmarks"]?.let { bookmarks ->
bookmarkFile.parentFile?.mkdirs()
bookmarkFile.writeText(cloudSidecarJson.encodeToString(JsonElement.serializer(), bookmarks))
bookmarkFile.setLastModified(timestamp)
} ?: run {
if (bookmarkFile.isFile) bookmarkFile.delete()
}
canonicalData["text"]?.let { richText ->
val richDocument = SharedPdfRichTextSerializer.decodeElement(richText)
richTextFile.parentFile?.mkdirs()
richTextFile.writeText(SharedPdfRichTextSerializer.encode(richDocument))
richTextFile.setLastModified(timestamp)
} ?: run {
if (richTextFile.isFile) richTextFile.delete()
}
return true
}
}
private val cloudSidecarJson = Json {
ignoreUnknownKeys = true
prettyPrint = true
encodeDefaults = true
}
private fun Json.parseElementOrNull(raw: String): JsonElement? {
return runCatching { parseToJsonElement(raw) }.getOrNull()
}
private fun JsonElement.jsonObjectOrNull(): JsonObject? {
if (this is JsonNull) return null
return runCatching { jsonObject }.getOrNull()
}
private fun JsonObject.hasPdfAnnotationPayload(): Boolean {
return containsKey(SharedPdfAnnotationSidecarCodec.KEY_PDF_ANNOTATIONS) ||
containsKey(SharedPdfAnnotationSidecarCodec.KEY_LEGACY_INK) ||
containsKey(SharedPdfAnnotationSidecarCodec.KEY_LEGACY_TEXT_BOXES) ||
containsKey(SharedPdfAnnotationSidecarCodec.KEY_LEGACY_HIGHLIGHTS)
}
private fun File.lastModifiedIfFile(): Long {
return if (isFile) lastModified() else 0L
}