* Add performance and stylus debugging logs * Refactor and decouple UI models from `MainViewModel` * Refactor library state management and projection logic * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Introduce ReaderEngine and enhance EPUB reader features in windows app * Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support. * Implement PDF rendering and text extraction for desktop using Pdfium * Add `NonReaderScreens.kt` and UI dependencies * Refactor and centralize library state management and models to improve cross-platform consistency * Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing * Implement PDF annotation system and enhanced zoom controls for the desktop viewer * Implement WebView-based EPUB rendering for desktop using CEF and embedded resources * Optimize UI state projection, navigation state handling, and main screen pager performance * Implement Bring Your Own Key (BYOK) support for AI features in OSS version * Support Gemini-based Cloud TTS with BYOK support for OSS builds * Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling. * crash fixes * Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6 * Implement dynamic bottom padding for the page info bar to account for device rounded corners * Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar` * Optimize PDF tiling performance and refine pan-and-fling gesture handling * Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers * Updated UI for customize toolbar * Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls * restructure PDF viewer UI component hierarchy to fix verifier crash * Implement separate text dimming factors for light and dark themes * Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers * Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning * Centralize file type resolution logic and implement HTML sanitization during import * Introduce vertical margin customization and configurable progress bar positioning * texture support in epub reader * Enhance TTS session management, progress tracking, and diagnostic logging * Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic. * Refine TTS page mapping for PDF and overhaul TTS control UI * Implement natural session completion logic in `TtsPlaybackManager` for cloud tts * Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen` * Refine TTS playback continuity across PDF pages and improve state management for session transitions * Implement global texture transparency and enhance textured theme support across PDF and EPUB readers. * Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode * Add Support Project screen * Optimize library performance via projection caching, batch database updates, and scoped folder synchronization. * Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic * Bump version to 1.0.47 (51)
205 lines
7.4 KiB
Kotlin
205 lines
7.4 KiB
Kotlin
package com.aryan.reader.desktop
|
|
|
|
import com.aryan.reader.shared.BookItem
|
|
import com.aryan.reader.shared.BookShelfRef
|
|
import com.aryan.reader.shared.FileType
|
|
import com.aryan.reader.shared.ShelfRecord
|
|
import com.aryan.reader.shared.Tag
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.serialization.json.JsonArray
|
|
import kotlinx.serialization.json.JsonElement
|
|
import kotlinx.serialization.json.JsonNull
|
|
import kotlinx.serialization.json.JsonObject
|
|
import kotlinx.serialization.json.JsonPrimitive
|
|
import kotlinx.serialization.json.booleanOrNull
|
|
import kotlinx.serialization.json.doubleOrNull
|
|
import kotlinx.serialization.json.floatOrNull
|
|
import kotlinx.serialization.json.jsonArray
|
|
import kotlinx.serialization.json.jsonObject
|
|
import kotlinx.serialization.json.jsonPrimitive
|
|
import kotlinx.serialization.json.longOrNull
|
|
import java.io.File
|
|
|
|
data class DesktopLibrarySnapshot(
|
|
val books: List<BookItem> = emptyList(),
|
|
val shelfRecords: List<ShelfRecord> = emptyList(),
|
|
val shelfRefs: List<BookShelfRef> = emptyList(),
|
|
val tags: List<Tag> = emptyList()
|
|
)
|
|
|
|
class DesktopLibraryDatabase(
|
|
private val databaseFile: File = defaultDatabaseFile()
|
|
) {
|
|
private val json = Json {
|
|
prettyPrint = true
|
|
ignoreUnknownKeys = true
|
|
}
|
|
|
|
fun load(): DesktopLibrarySnapshot {
|
|
if (!databaseFile.exists()) return DesktopLibrarySnapshot()
|
|
val root = runCatching {
|
|
json.parseToJsonElement(databaseFile.readText()).jsonObject
|
|
}.getOrNull() ?: return DesktopLibrarySnapshot()
|
|
|
|
return DesktopLibrarySnapshot(
|
|
books = root.array("books").mapNotNull { it.asBookItemOrNull() },
|
|
shelfRecords = root.array("shelves").mapNotNull { it.asShelfRecordOrNull() },
|
|
shelfRefs = root.array("bookShelfRefs").mapNotNull { it.asBookShelfRefOrNull() },
|
|
tags = root.array("tags").mapNotNull { it.asTagOrNull() }
|
|
)
|
|
}
|
|
|
|
fun save(snapshot: DesktopLibrarySnapshot) {
|
|
databaseFile.parentFile?.mkdirs()
|
|
val root = JsonObject(
|
|
mapOf(
|
|
"schemaVersion" to JsonPrimitive(1),
|
|
"books" to JsonArray(snapshot.books.map { it.toJsonObject() }),
|
|
"shelves" to JsonArray(snapshot.shelfRecords.map { it.toJsonObject() }),
|
|
"bookShelfRefs" to JsonArray(snapshot.shelfRefs.map { it.toJsonObject() }),
|
|
"tags" to JsonArray(snapshot.tags.map { it.toJsonObject() })
|
|
)
|
|
)
|
|
databaseFile.writeText(root.toString())
|
|
}
|
|
|
|
companion object {
|
|
fun defaultDatabaseFile(): File {
|
|
val baseDir = System.getenv("APPDATA")?.takeIf { it.isNotBlank() }
|
|
?: File(System.getProperty("user.home"), "AppData/Roaming").absolutePath
|
|
return File(baseDir, "Episteme/library.json")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun JsonObject.array(name: String): List<JsonElement> {
|
|
return runCatching { this[name]?.jsonArray?.toList().orEmpty() }.getOrDefault(emptyList())
|
|
}
|
|
|
|
private fun JsonObject.string(name: String): String? {
|
|
return this[name]?.takeUnless { it is JsonNull }?.jsonPrimitive?.content
|
|
}
|
|
|
|
private fun JsonObject.long(name: String, fallback: Long = 0L): Long {
|
|
return this[name]?.jsonPrimitive?.longOrNull ?: fallback
|
|
}
|
|
|
|
private fun JsonObject.float(name: String): Float? {
|
|
return this[name]?.jsonPrimitive?.floatOrNull
|
|
}
|
|
|
|
private fun JsonObject.double(name: String): Double? {
|
|
return this[name]?.jsonPrimitive?.doubleOrNull
|
|
}
|
|
|
|
private fun JsonObject.boolean(name: String, fallback: Boolean): Boolean {
|
|
return this[name]?.jsonPrimitive?.booleanOrNull ?: fallback
|
|
}
|
|
|
|
private fun JsonElement.asBookItemOrNull(): BookItem? {
|
|
val obj = runCatching { jsonObject }.getOrNull() ?: return null
|
|
val id = obj.string("id") ?: return null
|
|
val displayName = obj.string("displayName") ?: return null
|
|
val type = obj.string("type")?.let { runCatching { FileType.valueOf(it) }.getOrNull() } ?: FileType.UNKNOWN
|
|
return BookItem(
|
|
id = id,
|
|
path = obj.string("path"),
|
|
type = type,
|
|
displayName = displayName,
|
|
timestamp = obj.long("timestamp"),
|
|
title = obj.string("title"),
|
|
author = obj.string("author"),
|
|
progressPercentage = obj.float("progressPercentage"),
|
|
isRecent = obj.boolean("isRecent", true),
|
|
fileSize = obj.long("fileSize"),
|
|
sourceFolder = obj.string("sourceFolder"),
|
|
seriesName = obj.string("seriesName"),
|
|
seriesIndex = obj.double("seriesIndex"),
|
|
tags = obj.array("tags").mapNotNull { it.asTagOrNull() }
|
|
)
|
|
}
|
|
|
|
private fun JsonElement.asShelfRecordOrNull(): ShelfRecord? {
|
|
val obj = runCatching { jsonObject }.getOrNull() ?: return null
|
|
return ShelfRecord(
|
|
id = obj.string("id") ?: return null,
|
|
name = obj.string("name") ?: return null,
|
|
isSmart = obj.boolean("isSmart", false),
|
|
smartRulesJson = obj.string("smartRulesJson")
|
|
)
|
|
}
|
|
|
|
private fun JsonElement.asBookShelfRefOrNull(): BookShelfRef? {
|
|
val obj = runCatching { jsonObject }.getOrNull() ?: return null
|
|
return BookShelfRef(
|
|
bookId = obj.string("bookId") ?: return null,
|
|
shelfId = obj.string("shelfId") ?: return null,
|
|
addedAt = obj.long("addedAt")
|
|
)
|
|
}
|
|
|
|
private fun JsonElement.asTagOrNull(): Tag? {
|
|
val obj = runCatching { jsonObject }.getOrNull() ?: return null
|
|
return Tag(
|
|
id = obj.string("id") ?: return null,
|
|
name = obj.string("name") ?: return null,
|
|
color = obj["color"]?.takeUnless { it is JsonNull }?.jsonPrimitive?.content?.toIntOrNull()
|
|
)
|
|
}
|
|
|
|
private fun BookItem.toJsonObject(): JsonObject {
|
|
return JsonObject(
|
|
mapOf(
|
|
"id" to JsonPrimitive(id),
|
|
"path" to path.asJson(),
|
|
"type" to JsonPrimitive(type.name),
|
|
"displayName" to JsonPrimitive(displayName),
|
|
"timestamp" to JsonPrimitive(timestamp),
|
|
"title" to title.asJson(),
|
|
"author" to author.asJson(),
|
|
"progressPercentage" to progressPercentage.asJson(),
|
|
"isRecent" to JsonPrimitive(isRecent),
|
|
"fileSize" to JsonPrimitive(fileSize),
|
|
"sourceFolder" to sourceFolder.asJson(),
|
|
"seriesName" to seriesName.asJson(),
|
|
"seriesIndex" to seriesIndex.asJson(),
|
|
"tags" to JsonArray(tags.map { it.toJsonObject() })
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun ShelfRecord.toJsonObject(): JsonObject {
|
|
return JsonObject(
|
|
mapOf(
|
|
"id" to JsonPrimitive(id),
|
|
"name" to JsonPrimitive(name),
|
|
"isSmart" to JsonPrimitive(isSmart),
|
|
"smartRulesJson" to smartRulesJson.asJson()
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun BookShelfRef.toJsonObject(): JsonObject {
|
|
return JsonObject(
|
|
mapOf(
|
|
"bookId" to JsonPrimitive(bookId),
|
|
"shelfId" to JsonPrimitive(shelfId),
|
|
"addedAt" to JsonPrimitive(addedAt)
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun Tag.toJsonObject(): JsonObject {
|
|
return JsonObject(
|
|
mapOf(
|
|
"id" to JsonPrimitive(id),
|
|
"name" to JsonPrimitive(name),
|
|
"color" to color.asJson()
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun String?.asJson(): JsonElement = this?.let { JsonPrimitive(it) } ?: JsonNull
|
|
private fun Float?.asJson(): JsonElement = this?.let { JsonPrimitive(it) } ?: JsonNull
|
|
private fun Double?.asJson(): JsonElement = this?.let { JsonPrimitive(it) } ?: JsonNull
|
|
private fun Int?.asJson(): JsonElement = this?.let { JsonPrimitive(it) } ?: JsonNull
|