Desktop app (#308)
* Implement build profiles and feature policy for offline desktop builds * Introduce unified cross-platform Settings Hub * Refactor main settings into a hierarchical page-based navigation model * Refactor library projection to use shared multiplatform logic * Refactor UI state consumption by removing intermediate screen models * Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic * Refactor state management for tabs, selection, and pinning to use shared bridge logic * Refactor file type management and validation into a centralized shared module * Centralize file type resolution and improve handling of unknown types * Centralize book import logic with SharedImportPlanner * Refactor magnifier geometry logic and coordinate mapping * Properly handle orientation changes in scroll-locked PDF reader * Add screen orientation controls to EPUB and PDF readers * Implement right-to-left (RTL) pagination support and refactor reader menus * Separate right-to-left pagination settings for PDF and EPUB * Ensure PDF page data is scoped by document key for multi tab support * Implement theme-aware link styling for the epub reader * Implement jump history for back and forward navigation in the epub reader * Improve locator handling and navigation logic in paginated reader mode * Implement stable pagination navigation and location tracking * Centralize banner message management and auto-dismiss logic in MainViewModel * Implement zoom and pan state preservation for PDF pan lock mode * Enhance reader navigation UI and workspace layout management in desktop app * Refactor reader navigation sidebar and relocate search controls in desktop app * Enhance reader UI with redesigned selection menus and bottom sheet overlays * Implement custom highlight palettes and reader theme customization in desktop app * Implement cross-platform modal layer and refine reader UI styling * Improve highlight accuracy and implement metadata enrichment on book open in desktop app * Implement two-page spread layout for paginated reader on desktop * Implement persistent caching for book loading and pagination in desktop app * Implement persistent caching for book loading and pagination in desktop app * Optimize reader settings updates by separating layout and appearance changes in desktop app * Improve desktop window branding and native Windows styling * Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app * Refine selection handle positioning and interaction logic * Implement EPUB selection debug logging and improve handle targeting * Optimize desktop book loading performance and UI responsiveness * Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer. * Implement smooth zoom preview for the PDF reader in desktop app * Optimize PDF rendering performance and responsiveness in the desktop reader * Implement conditional diagnostic logging and update desktop build configuration * Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling * Added management options for annotations and highlights in the sidebar in desktop app * Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability. * Refined library filters and enhanced OPDS functionality in desktop app * Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app * Added PPTX support including document parsing, rendering, and indexing * Improved PPTX rendering and layout accuracy * Implemented text autofit support for PPTX rendering * Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity * Improved EPUB pagination accuracy and added layout telemetry in desktop app * Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app * Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling * Implemented billing account obfuscation * Implemented hierarchical folder navigation and improved library selection functionality in desktop app * Implemented platform-aware directory resolution and multi-platform native library support for desktop * Added full-screen mode for the reader workspace * Added PDF zoom indicator and interactive vertical scrollbar with page tooltips * Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management * Updated PDF indexing to replace existing page text and removed search result item keys * Implemented "preparing" foreground notification for TTS service * Optimized PDF rendering performance by pre-calculating page-specific annotations * Refactored desktop packaging tasks and improved distribution configuration * Optimized EPUB parser memory usage and added path traversal protection * Refactored WorkManager monitoring logic and added work pruning * Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks * Implemented bitmap size limits and scaling to prevent canvas rendering errors * Split long text paragraphs into multiple semantic blocks during HTML parsing * Implemented local ActionMode for text selection to prevent platform crashes * Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic * Added desktop startup splash screen and deferred WebView initialization * Reorganized settings hub and added separate PDF reader defaults * Implemented embedded cover extraction and metadata support for MOBI and FB2 formats * Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance. * Implemented procedurally generated book covers and replaced static placeholders * Redesigned search UI with a top bar and results overlay in desktop app * Added PDF page gap and overlay visibility options and implemented DesktopBookImporter * Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop * Implemented PDF viewport persistence for zoom and scroll positions in desktop app * Improved desktop fullscreen implementation and state restoration * Implemented desktop window state persistence * Implemented flavor-based branding and ProGuard configuration for desktop builds * Implemented precise reader positioning and improved highlight rendering logic in desktop app * Added support for user-editable book metadata * Enhanced book metadata support and integrated info/edit dialogs * Implemented embedded EPUB metadata editing * Improved highlight mapping and added custom scrollbar styling for the reader. * Reduced desktop WebView bundle size by excluding unused locales and runtime files * Added neutral pan mode as the default PDF interaction state. * Refactored library empty states and updated primary navigation tabs in desktop app * Implemented native paginated reader and unified content rendering architecture in desktop epub reader * Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing. * Improved pagination overflow detection in desktop * Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
This commit is contained in:
parent
c0d0e57e79
commit
b20ade9946
247 changed files with 43321 additions and 7087 deletions
|
|
@ -86,6 +86,17 @@ class DesktopAiByokStoreTest {
|
|||
assertEquals(GEMINI_CLOUD_TTS_MODEL_ID, loaded.ttsModel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load does not probe secure storage when settings file is missing`() {
|
||||
val settingsFile = Files.createTempDirectory("reader-ai-store-missing").resolve("ai-byok.properties")
|
||||
val store = DesktopAiByokStore(settingsFile.toFile(), ThrowingAvailabilitySecretCodec)
|
||||
|
||||
val loaded = store.load()
|
||||
|
||||
assertEquals("", loaded.geminiKey)
|
||||
assertEquals("", loaded.groqKey)
|
||||
}
|
||||
|
||||
private object ReversibleSecretCodec : DesktopSecretCodec {
|
||||
override val isAvailable: Boolean = true
|
||||
|
||||
|
|
@ -103,4 +114,12 @@ class DesktopAiByokStoreTest {
|
|||
override fun protect(value: String): String = ""
|
||||
override fun unprotect(value: String): String = ""
|
||||
}
|
||||
|
||||
private object ThrowingAvailabilitySecretCodec : DesktopSecretCodec {
|
||||
override val isAvailable: Boolean
|
||||
get() = error("Secure storage should not be checked for a missing settings file.")
|
||||
|
||||
override fun protect(value: String): String = ""
|
||||
override fun unprotect(value: String): String = ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import com.aryan.reader.shared.ImportedBookFile
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.security.MessageDigest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class DesktopBookImporterTest {
|
||||
@Test
|
||||
fun `prepare imports copies supported file into app storage without source folder`() {
|
||||
val tempRoot = Files.createTempDirectory("episteme-book-importer-test").toFile()
|
||||
try {
|
||||
val source = File(tempRoot, "Book.md").apply { writeText("# Hello") }
|
||||
val store = File(tempRoot, "books")
|
||||
val importer = DesktopBookImporter(store)
|
||||
|
||||
val result = importer.prepareImports(listOf(source.toImportedBookFile()))
|
||||
|
||||
assertEquals(0, result.failedCount)
|
||||
val prepared = result.files.single()
|
||||
val copied = File(assertNotNull(prepared.localPath))
|
||||
assertEquals("Book.md", prepared.name)
|
||||
assertEquals(source.sha256(), prepared.id)
|
||||
assertNull(prepared.uriString)
|
||||
assertNull(prepared.sourceFolder)
|
||||
assertEquals(store.canonicalFile, copied.parentFile.canonicalFile)
|
||||
assertNotEquals(source.canonicalFile, copied.canonicalFile)
|
||||
assertEquals("# Hello", copied.readText())
|
||||
} finally {
|
||||
tempRoot.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `prepare imports leaves unsupported files uncopied for shared planner`() {
|
||||
val tempRoot = Files.createTempDirectory("episteme-book-importer-test").toFile()
|
||||
try {
|
||||
val source = File(tempRoot, "Archive.zip").apply { writeText("zip") }
|
||||
val store = File(tempRoot, "books")
|
||||
val importer = DesktopBookImporter(store)
|
||||
|
||||
val result = importer.prepareImports(listOf(source.toImportedBookFile(sourceFolder = tempRoot.absolutePath)))
|
||||
|
||||
assertEquals(0, result.failedCount)
|
||||
val prepared = result.files.single()
|
||||
assertEquals(source.absolutePath, prepared.localPath)
|
||||
assertNull(prepared.sourceFolder)
|
||||
assertNull(prepared.id)
|
||||
assertFalse(store.listFiles().orEmpty().any { it.isFile })
|
||||
} finally {
|
||||
tempRoot.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.toImportedBookFile(sourceFolder: String? = null): ImportedBookFile {
|
||||
return ImportedBookFile(
|
||||
name = name,
|
||||
uriString = null,
|
||||
localPath = absolutePath,
|
||||
size = length(),
|
||||
sourceFolder = sourceFolder
|
||||
)
|
||||
}
|
||||
|
||||
private fun File.sha256(): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(readBytes())
|
||||
return digest.digest().joinToString(separator = "") { byte -> "%02x".format(byte.toInt() and 0xff) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import com.aryan.reader.shared.SharedFeaturePolicy
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopBuildProfileTest {
|
||||
@Test
|
||||
fun `standard desktop flavor keeps online features available`() {
|
||||
val profile = desktopBuildProfileForFlavor("standard")
|
||||
|
||||
assertEquals(DesktopFlavorStandard, profile.flavor)
|
||||
assertEquals(EpistemeDesktopStandardAppName, profile.appName)
|
||||
assertEquals("Standard edition", profile.buildLabel)
|
||||
assertEquals(SharedFeaturePolicy.Standard, profile.featurePolicy)
|
||||
assertTrue(profile.featurePolicy.networkAccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `oss offline desktop flavor disables network backed features`() {
|
||||
val profile = desktopBuildProfileForFlavor("oss-offline")
|
||||
|
||||
assertEquals(DesktopFlavorOssOffline, profile.flavor)
|
||||
assertEquals(EpistemeDesktopOssAppName, profile.appName)
|
||||
assertEquals("Offline OSS edition", profile.buildLabel)
|
||||
assertEquals(SharedFeaturePolicy.OssOffline, profile.featurePolicy)
|
||||
assertFalse(profile.featurePolicy.networkAccess)
|
||||
assertFalse(profile.featurePolicy.aiAndCloud)
|
||||
assertFalse(profile.featurePolicy.opdsCatalogs)
|
||||
assertFalse(profile.featurePolicy.googleFontsDownload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `oss desktop flavor aliases resolve to offline oss profile`() {
|
||||
val profile = desktopBuildProfileForFlavor("oss")
|
||||
|
||||
assertEquals(DesktopFlavorOssOffline, profile.flavor)
|
||||
assertEquals(EpistemeDesktopOssAppName, profile.appName)
|
||||
assertEquals(SharedFeaturePolicy.OssOffline, profile.featurePolicy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop diagnostics are disabled unless explicitly enabled`() {
|
||||
assertFalse(desktopDiagnosticsFlag(null))
|
||||
assertFalse(desktopDiagnosticsFlag(""))
|
||||
assertFalse(desktopDiagnosticsFlag("false"))
|
||||
assertFalse(desktopDiagnosticsFlag("1"))
|
||||
|
||||
assertTrue(desktopDiagnosticsFlag("true"))
|
||||
assertTrue(desktopDiagnosticsFlag(" TRUE "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bundled webview detection requires cef binaries`() {
|
||||
val dir = Files.createTempDirectory("episteme-kcef-test").toFile()
|
||||
try {
|
||||
val windowsX64 = DesktopPlatform(DesktopOperatingSystem.WINDOWS, DesktopArchitecture.X64)
|
||||
assertFalse(isBundledDesktopWebViewPresent(dir, windowsX64))
|
||||
File(dir, "jcef.dll").writeText("jcef")
|
||||
File(dir, "libcef.dll").writeText("cef")
|
||||
|
||||
assertTrue(isBundledDesktopWebViewPresent(dir, windowsX64))
|
||||
} finally {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linux bundled webview detection requires cef shared library and resources`() {
|
||||
val dir = Files.createTempDirectory("episteme-linux-kcef-test").toFile()
|
||||
try {
|
||||
val linuxX64 = DesktopPlatform(DesktopOperatingSystem.LINUX, DesktopArchitecture.X64)
|
||||
assertFalse(isBundledDesktopWebViewPresent(dir, linuxX64))
|
||||
|
||||
File(dir, "libcef.so").writeText("cef")
|
||||
File(dir, "chrome-sandbox").writeText("sandbox")
|
||||
File(dir, "icudtl.dat").writeText("icu")
|
||||
File(dir, "locales").mkdir()
|
||||
|
||||
assertTrue(isBundledDesktopWebViewPresent(dir, linuxX64))
|
||||
} finally {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,21 @@ class DesktopCustomFontStoreTest {
|
|||
assertEquals(listOf("Inter", "Literata"), googleFontsFromJson("""["Inter", "", " Literata "]"""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `download google font fails before network when downloads are disabled`() {
|
||||
val tempRoot = Files.createTempDirectory("episteme-font-store-test").toFile()
|
||||
try {
|
||||
val store = DesktopCustomFontStore(
|
||||
fontsDir = File(tempRoot, "store"),
|
||||
googleFontsDownloadAvailable = { false }
|
||||
)
|
||||
|
||||
assertTrue(store.downloadGoogleFont("Inter").isFailure)
|
||||
} finally {
|
||||
tempRoot.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.toFontItem(): CustomFontItem {
|
||||
return CustomFontItem(
|
||||
id = nameWithoutExtension,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ class DesktopFolderMetadataExtractorTest {
|
|||
<metadata>
|
||||
<dc:title>Direct EPUB</dc:title>
|
||||
<dc:creator>Ada Lovelace</dc:creator>
|
||||
<dc:description><p>Metadata summary</p></dc:description>
|
||||
<meta name="calibre:series" content="Computing Notes" />
|
||||
<meta name="calibre:series_index" content="2" />
|
||||
<meta name="cover" content="cover-image" />
|
||||
</metadata>
|
||||
<manifest>
|
||||
|
|
@ -42,12 +45,49 @@ class DesktopFolderMetadataExtractorTest {
|
|||
val enriched = result.books.single()
|
||||
assertEquals("Direct EPUB", enriched.title)
|
||||
assertEquals("Ada Lovelace", enriched.author)
|
||||
assertEquals("<p>Metadata summary</p>", enriched.description)
|
||||
assertEquals("Computing Notes", enriched.seriesName)
|
||||
assertEquals(2.0, enriched.seriesIndex)
|
||||
assertEquals("Direct EPUB", enriched.originalTitle)
|
||||
assertEquals("Ada Lovelace", enriched.originalAuthor)
|
||||
assertEquals("Computing Notes", enriched.originalSeriesName)
|
||||
assertEquals(2.0, enriched.originalSeriesIndex)
|
||||
assertEquals("<p>Metadata summary</p>", enriched.originalDescription)
|
||||
assertEquals(epub.lastModified(), enriched.fileContentModifiedTimestamp)
|
||||
assertTrue(enriched.folderTextMetadataParsed)
|
||||
assertTrue(File(assertNotNull(enriched.coverImagePath)).isFile)
|
||||
assertEquals(1, result.stats.updatedBooks)
|
||||
assertEquals(1, result.stats.coversUpdated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `opened epub gets embedded cover`() = withCoverCacheDir { tempDir ->
|
||||
val epub = File(tempDir, "opened.epub")
|
||||
writeEpub(
|
||||
target = epub,
|
||||
opf = """
|
||||
<package xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata>
|
||||
<dc:title>Opened EPUB</dc:title>
|
||||
<dc:creator>Mary Shelley</dc:creator>
|
||||
<meta name="cover" content="cover-image" />
|
||||
</metadata>
|
||||
<manifest>
|
||||
<item id="cover-image" href="images/cover.png" media-type="image/png" />
|
||||
</manifest>
|
||||
</package>
|
||||
""".trimIndent()
|
||||
)
|
||||
val book = bookFor(epub, FileType.EPUB, title = null)
|
||||
|
||||
val enriched = DesktopFolderMetadataExtractor.enrichOpenedBook(book)
|
||||
|
||||
assertEquals("Opened EPUB", enriched.title)
|
||||
assertEquals("Mary Shelley", enriched.author)
|
||||
assertTrue(enriched.folderTextMetadataParsed)
|
||||
assertTrue(File(assertNotNull(enriched.coverImagePath)).isFile)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `direct imported text file gets generated cover`() = withCoverCacheDir { tempDir ->
|
||||
val textFile = File(tempDir, "notes.txt").apply { writeText("Notes") }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import com.aryan.reader.shared.BookItem
|
||||
import com.aryan.reader.shared.FileType
|
||||
import com.aryan.reader.shared.LOCAL_FOLDER_SYNC_DATA_DIR
|
||||
import com.aryan.reader.shared.SharedFolderBookMetadata
|
||||
import com.aryan.reader.shared.SharedReaderScreenState
|
||||
import com.aryan.reader.shared.SyncedFolder
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class DesktopLocalFolderSyncTest {
|
||||
@Test
|
||||
fun `metadata-only sync imports sidecar metadata without scanning physical files`() {
|
||||
val root = Files.createTempDirectory("reader-desktop-folder-sync").toFile()
|
||||
try {
|
||||
File(root, "New.pdf").writeText("%PDF")
|
||||
val existingFile = File(root, "Existing.pdf")
|
||||
val existingId = "local_Existing.pdf"
|
||||
writeMetadataSidecar(
|
||||
root = root,
|
||||
metadata = metadata(
|
||||
id = existingId,
|
||||
title = "Remote Title",
|
||||
progress = 72f,
|
||||
modified = 2_000L
|
||||
)
|
||||
)
|
||||
|
||||
val existingBook = BookItem(
|
||||
id = existingId,
|
||||
path = existingFile.absolutePath,
|
||||
type = FileType.PDF,
|
||||
displayName = existingFile.name,
|
||||
timestamp = 100L,
|
||||
title = "Local Title",
|
||||
progressPercentage = 5f,
|
||||
sourceFolder = root.absolutePath
|
||||
)
|
||||
|
||||
val result = DesktopLocalFolderSync.sync(
|
||||
state = SharedReaderScreenState(
|
||||
rawLibraryBooks = listOf(existingBook),
|
||||
syncedFolders = listOf(syncedFolder(root))
|
||||
),
|
||||
shelfRefs = emptyList(),
|
||||
nowMillis = 3_000L,
|
||||
metadataOnly = true
|
||||
)
|
||||
|
||||
assertEquals(1, result.state.rawLibraryBooks.size)
|
||||
val syncedBook = result.state.rawLibraryBooks.single()
|
||||
assertEquals(existingId, syncedBook.id)
|
||||
assertEquals("Local Title", syncedBook.title)
|
||||
assertEquals(72f, syncedBook.progressPercentage)
|
||||
assertNull(result.state.rawLibraryBooks.firstOrNull { it.id == "local_New.pdf" })
|
||||
assertEquals(0, result.stats.scannedFiles)
|
||||
assertEquals(0, result.stats.newBooks)
|
||||
assertEquals(0, result.stats.removedBooks)
|
||||
assertEquals(1, result.stats.remoteMetadataUpdates)
|
||||
} finally {
|
||||
root.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncedFolder(root: File): SyncedFolder {
|
||||
return SyncedFolder(
|
||||
uriString = root.absolutePath,
|
||||
name = root.name,
|
||||
lastScanTime = 0L,
|
||||
allowedFileTypes = setOf(FileType.PDF)
|
||||
)
|
||||
}
|
||||
|
||||
private fun writeMetadataSidecar(root: File, metadata: SharedFolderBookMetadata) {
|
||||
val syncDir = File(root, LOCAL_FOLDER_SYNC_DATA_DIR).apply { mkdirs() }
|
||||
File(syncDir, ".${metadata.bookId}.json").writeText(metadata.toJsonString())
|
||||
}
|
||||
|
||||
private fun metadata(
|
||||
id: String,
|
||||
title: String,
|
||||
progress: Float,
|
||||
modified: Long
|
||||
): SharedFolderBookMetadata {
|
||||
return SharedFolderBookMetadata(
|
||||
bookId = id,
|
||||
title = title,
|
||||
author = null,
|
||||
displayName = "Existing.pdf",
|
||||
type = FileType.PDF.name,
|
||||
lastChapterIndex = null,
|
||||
lastPage = null,
|
||||
lastPositionCfi = null,
|
||||
progressPercentage = progress,
|
||||
isRecent = true,
|
||||
lastModifiedTimestamp = modified,
|
||||
bookmarksJson = null,
|
||||
locatorBlockIndex = null,
|
||||
locatorCharOffset = null,
|
||||
customName = null,
|
||||
highlightsJson = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import java.io.File
|
|||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopOpdsRepositoryTest {
|
||||
|
|
@ -27,6 +28,45 @@ class DesktopOpdsRepositoryTest {
|
|||
assertEquals("pass", custom.password)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop opds http blocks before network in offline flavor`() {
|
||||
withSystemProperty(DesktopFlavorProperty, DesktopFlavorOssOffline) {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
DesktopOpdsHttp.fetchString("https://example.org/opds", null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop opds http creates basic authorization header for challenged catalogs`() {
|
||||
assertEquals(
|
||||
"Basic dXNlcjpwYXNz",
|
||||
DesktopOpdsHttp.authorizationHeaderForChallenge(
|
||||
challenge = "Basic realm=\"Catalog\"",
|
||||
url = "https://example.org/opds",
|
||||
username = "user",
|
||||
password = "pass"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop opds http creates digest authorization header for challenged catalogs`() {
|
||||
assertEquals(
|
||||
"Digest username=\"Mufasa\", realm=\"testrealm@host.com\", nonce=\"abcdef\", " +
|
||||
"uri=\"/dir/index.atom?x=1\", response=\"ca833912ad1f4339630e23476d538d67\", " +
|
||||
"qop=auth, nc=00000001, cnonce=\"0a4f113b\", opaque=\"xyz\"",
|
||||
DesktopOpdsHttp.authorizationHeaderForChallenge(
|
||||
challenge = "Digest realm=\"testrealm@host.com\", nonce=\"abcdef\", qop=\"auth\", opaque=\"xyz\"",
|
||||
url = "https://example.org/dir/index.atom?x=1",
|
||||
username = "Mufasa",
|
||||
password = "Circle Of Life",
|
||||
cnonce = "0a4f113b",
|
||||
nonceCount = "00000001"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DesktopOpdsRepository.addCatalogForTest(
|
||||
title: String,
|
||||
url: String,
|
||||
|
|
@ -53,4 +93,26 @@ class DesktopOpdsRepositoryTest {
|
|||
dir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun withSystemProperty(
|
||||
key: String,
|
||||
value: String?,
|
||||
block: () -> Unit
|
||||
) {
|
||||
val previous = System.getProperty(key)
|
||||
try {
|
||||
if (value == null) {
|
||||
System.clearProperty(key)
|
||||
} else {
|
||||
System.setProperty(key, value)
|
||||
}
|
||||
block()
|
||||
} finally {
|
||||
if (previous == null) {
|
||||
System.clearProperty(key)
|
||||
} else {
|
||||
System.setProperty(key, previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aryan.reader.shared.PdfDisplayMode
|
||||
import com.aryan.reader.shared.ReaderTheme
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DesktopPdfThemeTest {
|
||||
@Test
|
||||
fun `desktop pdf defaults to vertical display mode`() {
|
||||
assertEquals(PdfDisplayMode.VERTICAL_SCROLL, DesktopDefaultPdfDisplayMode)
|
||||
assertEquals(8.dp, DesktopDefaultPdfVerticalPageGap)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `page background follows android pdf theme defaults`() {
|
||||
val noTheme = ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false)
|
||||
val reverse = ReaderTheme("reverse", "Reverse", Color.Black, Color.White, true)
|
||||
val sepia = ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false)
|
||||
|
||||
assertEquals(Color.White, desktopPdfPageBackgroundColor(noTheme, PdfDisplayMode.VERTICAL_SCROLL))
|
||||
assertEquals(Color.Black, desktopPdfPageBackgroundColor(noTheme, PdfDisplayMode.PAGINATION))
|
||||
assertEquals(Color.Black, desktopPdfPageBackgroundColor(reverse, PdfDisplayMode.VERTICAL_SCROLL))
|
||||
assertEquals(Color.White, desktopPdfPageBackgroundColor(reverse, PdfDisplayMode.PAGINATION))
|
||||
assertEquals(Color(0xFFFBF0D9), desktopPdfPageBackgroundColor(sepia, PdfDisplayMode.VERTICAL_SCROLL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vertical viewport uses app gap color only when page gaps are visible`() {
|
||||
val pageBackground = Color.White
|
||||
val gapBackground = Color(0xFFE2E2E2)
|
||||
|
||||
assertEquals(
|
||||
gapBackground,
|
||||
desktopPdfVerticalViewportBackgroundColor(
|
||||
pageBackgroundColor = pageBackground,
|
||||
gapBackgroundColor = gapBackground,
|
||||
isPageGapVisible = true
|
||||
)
|
||||
)
|
||||
assertEquals(
|
||||
pageBackground,
|
||||
desktopPdfVerticalViewportBackgroundColor(
|
||||
pageBackgroundColor = pageBackground,
|
||||
gapBackgroundColor = gapBackground,
|
||||
isPageGapVisible = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DesktopPlatformPathsTest {
|
||||
@Test
|
||||
fun `desktop platform detects linux x64 resource names`() {
|
||||
val platform = currentDesktopPlatform(osName = "Linux", osArch = "amd64")
|
||||
|
||||
assertEquals(DesktopOperatingSystem.LINUX, platform.os)
|
||||
assertEquals(DesktopArchitecture.X64, platform.architecture)
|
||||
assertEquals("kcef-bundle-linux-x64", platform.kcefBundleDirectoryName)
|
||||
assertEquals("linux-x64-v8", platform.pdfiumDirectoryName)
|
||||
assertEquals("lib", platform.pdfiumLibraryDirectoryName)
|
||||
assertEquals("libpdfium.so", platform.pdfiumLibraryFileName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop platform keeps existing windows resource names`() {
|
||||
val platform = currentDesktopPlatform(osName = "Windows 11", osArch = "amd64")
|
||||
|
||||
assertEquals(DesktopOperatingSystem.WINDOWS, platform.os)
|
||||
assertEquals(DesktopArchitecture.X64, platform.architecture)
|
||||
assertEquals("kcef-bundle", platform.kcefBundleDirectoryName)
|
||||
assertEquals("win-x64-v8", platform.pdfiumDirectoryName)
|
||||
assertEquals("bin", platform.pdfiumLibraryDirectoryName)
|
||||
assertEquals("pdfium.dll", platform.pdfiumLibraryFileName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linux user directories follow xdg environment variables`() {
|
||||
val platform = DesktopPlatform(DesktopOperatingSystem.LINUX, DesktopArchitecture.X64)
|
||||
val env = mapOf(
|
||||
"XDG_DATA_HOME" to "/tmp/xdg-data",
|
||||
"XDG_CONFIG_HOME" to "/tmp/xdg-config",
|
||||
"XDG_CACHE_HOME" to "/tmp/xdg-cache"
|
||||
)
|
||||
|
||||
assertEquals("/tmp/xdg-data/episteme", desktopUserDataRoot(platform, env::get, "/home/reader").portablePath())
|
||||
assertEquals("/tmp/xdg-config/episteme", desktopUserConfigRoot(platform, env::get, "/home/reader").portablePath())
|
||||
assertEquals("/tmp/xdg-cache/episteme", desktopUserCacheRoot(platform, env::get, "/home/reader").portablePath())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linux user directories ignore relative xdg environment values`() {
|
||||
val platform = DesktopPlatform(DesktopOperatingSystem.LINUX, DesktopArchitecture.X64)
|
||||
val env = mapOf(
|
||||
"XDG_DATA_HOME" to "relative-data",
|
||||
"XDG_CONFIG_HOME" to "relative-config",
|
||||
"XDG_CACHE_HOME" to "relative-cache"
|
||||
)
|
||||
|
||||
assertEquals("/home/reader/.local/share/episteme", desktopUserDataRoot(platform, env::get, "/home/reader").portablePath())
|
||||
assertEquals("/home/reader/.config/episteme", desktopUserConfigRoot(platform, env::get, "/home/reader").portablePath())
|
||||
assertEquals("/home/reader/.cache/episteme", desktopUserCacheRoot(platform, env::get, "/home/reader").portablePath())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `windows user directories keep appdata compatible root`() {
|
||||
val platform = DesktopPlatform(DesktopOperatingSystem.WINDOWS, DesktopArchitecture.X64)
|
||||
val env = mapOf("APPDATA" to "C:/Users/reader/AppData/Roaming")
|
||||
|
||||
assertEquals("C:/Users/reader/AppData/Roaming/Episteme", desktopUserDataRoot(platform, env::get, "C:/Users/reader").portablePath())
|
||||
assertEquals("C:/Users/reader/AppData/Roaming/Episteme", desktopUserConfigRoot(platform, env::get, "C:/Users/reader").portablePath())
|
||||
assertEquals("C:/Users/reader/AppData/Roaming/Episteme", desktopUserCacheRoot(platform, env::get, "C:/Users/reader").portablePath())
|
||||
}
|
||||
}
|
||||
|
||||
private fun java.io.File.portablePath(): String {
|
||||
return path.replace('\\', '/')
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.aryan.reader.shared.BookItem
|
||||
import com.aryan.reader.shared.FileType
|
||||
import com.aryan.reader.shared.ReaderPlatform
|
||||
import com.aryan.reader.shared.SharedFileCapabilities
|
||||
import com.aryan.reader.shared.pdf.PdfZoomSpec
|
||||
import com.aryan.reader.shared.reader.ReaderReadingMode
|
||||
import com.aryan.reader.shared.reader.ReaderSettings
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopReaderDefaultsTest {
|
||||
|
||||
@Test
|
||||
fun `desktop open book dialog accepts every shared desktop readable format`() {
|
||||
assertEquals(
|
||||
SharedFileCapabilities.readableTypesFor(ReaderPlatform.DESKTOP),
|
||||
desktopBookFileTypesForDialog()
|
||||
)
|
||||
assertTrue(FileType.PDF in desktopBookFileTypesForDialog())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop uses global reader defaults when book has no local settings`() {
|
||||
val defaults = ReaderSettings(fontSize = 23, readingMode = ReaderReadingMode.VERTICAL)
|
||||
val book = bookItem("without-local")
|
||||
|
||||
assertEquals(defaults, resolvedDesktopReaderSettings(book, defaults))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop keeps local book reader settings ahead of global defaults`() {
|
||||
val defaults = ReaderSettings(fontSize = 23, readingMode = ReaderReadingMode.VERTICAL)
|
||||
val local = ReaderSettings(fontSize = 17, readingMode = ReaderReadingMode.PAGINATED, themeId = "sepia")
|
||||
val book = bookItem("with-local").copy(readerSettings = local)
|
||||
|
||||
assertEquals(local, resolvedDesktopReaderSettings(book, defaults))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop pdf zoom allows deeper page magnification`() {
|
||||
val sharedDefaultMax = PdfZoomSpec().max
|
||||
val letterPageScale = DesktopPdfZoomSpec.safeRenderScale(
|
||||
pageWidth = 612f,
|
||||
pageHeight = 792f,
|
||||
requestedScale = 6f
|
||||
)
|
||||
|
||||
assertEquals(8f, DesktopPdfZoomSpec.max)
|
||||
assertTrue(letterPageScale > sharedDefaultMax)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop pdf touchpad zoom factors zoom in and out`() {
|
||||
val zoomSpec = PdfZoomSpec(min = 0.5f, max = 8f, default = 1f)
|
||||
|
||||
assertTrue(desktopPdfScrollZoomFactor(-1f) > 1.1f)
|
||||
assertTrue(desktopPdfScrollZoomFactor(1f) < 0.9f)
|
||||
assertEquals(8f, desktopPdfZoomTarget(currentZoom = 7.8f, zoomSpec = zoomSpec, factor = 2f))
|
||||
assertEquals(0.5f, desktopPdfZoomTarget(currentZoom = 0.6f, zoomSpec = zoomSpec, factor = 0.1f))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop paginated pdf page changes avoid high resolution first render`() {
|
||||
assertEquals(
|
||||
DesktopPdfPaginationFastFirstRenderMaxScale,
|
||||
desktopPdfPaginationFirstRenderScale(requestedScale = 6f, hasPageRender = false)
|
||||
)
|
||||
assertEquals(
|
||||
6f,
|
||||
desktopPdfPaginationFirstRenderScale(
|
||||
requestedScale = 6f,
|
||||
hasPageRender = false,
|
||||
isOpeningRender = true
|
||||
)
|
||||
)
|
||||
assertEquals(
|
||||
6f,
|
||||
desktopPdfPaginationFirstRenderScale(requestedScale = 6f, hasPageRender = true)
|
||||
)
|
||||
assertEquals(
|
||||
1.25f,
|
||||
desktopPdfPaginationFirstRenderScale(requestedScale = 1.25f, hasPageRender = false)
|
||||
)
|
||||
assertEquals(
|
||||
0.75f,
|
||||
desktopPdfPaginationFirstRenderScale(requestedScale = 0.75f, hasPageRender = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop pdf anchored zoom keeps cursor content stable`() {
|
||||
assertEquals(
|
||||
300,
|
||||
desktopPdfAnchoredScrollTarget(currentScroll = 100, anchor = 100f, oldZoom = 1f, newZoom = 2f)
|
||||
)
|
||||
assertEquals(
|
||||
25,
|
||||
desktopPdfAnchoredScrollTarget(currentScroll = 150, anchor = 100f, oldZoom = 2f, newZoom = 1f)
|
||||
)
|
||||
assertEquals(
|
||||
100,
|
||||
desktopPdfAnchoredLazyItemScrollOffset(itemOffset = 0, anchor = 100f, oldZoom = 1f, newZoom = 2f)
|
||||
)
|
||||
assertEquals(
|
||||
200,
|
||||
desktopPdfAnchoredLazyItemScrollOffset(itemOffset = -50, anchor = 100f, oldZoom = 1f, newZoom = 2f)
|
||||
)
|
||||
assertEquals(
|
||||
IntOffset(100, 100),
|
||||
desktopPdfAnchoredPageScrollDelta(
|
||||
viewportRootOffset = Offset.Zero,
|
||||
oldPageRootOffset = Offset.Zero,
|
||||
currentPageRootOffset = Offset.Zero,
|
||||
anchor = Offset(100f, 100f),
|
||||
oldZoom = 1f,
|
||||
newZoom = 2f
|
||||
)
|
||||
)
|
||||
assertEquals(
|
||||
IntOffset(0, 0),
|
||||
desktopPdfAnchoredPageScrollDelta(
|
||||
viewportRootOffset = Offset.Zero,
|
||||
oldPageRootOffset = Offset.Zero,
|
||||
currentPageRootOffset = Offset(-100f, -100f),
|
||||
anchor = Offset(100f, 100f),
|
||||
oldZoom = 1f,
|
||||
newZoom = 2f
|
||||
)
|
||||
)
|
||||
val offCenterPivot = desktopPdfZoomPreviewPivotFraction(
|
||||
viewportRootOffset = Offset(20f, 30f),
|
||||
pageRootOffset = Offset(120f, 230f),
|
||||
anchor = Offset(250f, 450f),
|
||||
pageCanvasSize = IntSize(500, 1000)
|
||||
) ?: error("Expected off-center pivot")
|
||||
assertEquals(0.3f, offCenterPivot.x, 0.0001f)
|
||||
assertEquals(0.25f, offCenterPivot.y, 0.0001f)
|
||||
|
||||
val clampedPivot = desktopPdfZoomPreviewPivotFraction(
|
||||
viewportRootOffset = Offset.Zero,
|
||||
pageRootOffset = Offset.Zero,
|
||||
anchor = Offset(900f, -20f),
|
||||
pageCanvasSize = IntSize(500, 1000)
|
||||
) ?: error("Expected clamped pivot")
|
||||
assertEquals(1f, clampedPivot.x, 0.0001f)
|
||||
assertEquals(0f, clampedPivot.y, 0.0001f)
|
||||
|
||||
val firstPageDocumentTranslation = desktopPdfDocumentZoomPreviewTranslation(
|
||||
viewportRootOffset = Offset.Zero,
|
||||
pageRootOffset = Offset(0f, 0f),
|
||||
anchor = Offset(100f, 200f),
|
||||
previewScale = 2f
|
||||
) ?: error("Expected first page document translation")
|
||||
assertEquals(-100f, firstPageDocumentTranslation.x, 0.0001f)
|
||||
assertEquals(-200f, firstPageDocumentTranslation.y, 0.0001f)
|
||||
|
||||
val secondPageDocumentTranslation = desktopPdfDocumentZoomPreviewTranslation(
|
||||
viewportRootOffset = Offset.Zero,
|
||||
pageRootOffset = Offset(0f, 900f),
|
||||
anchor = Offset(100f, 200f),
|
||||
previewScale = 2f
|
||||
) ?: error("Expected second page document translation")
|
||||
assertEquals(-100f, secondPageDocumentTranslation.x, 0.0001f)
|
||||
assertEquals(700f, secondPageDocumentTranslation.y, 0.0001f)
|
||||
}
|
||||
|
||||
private fun bookItem(id: String): BookItem {
|
||||
return BookItem(
|
||||
id = id,
|
||||
path = "C:/Books/$id.epub",
|
||||
type = FileType.EPUB,
|
||||
displayName = "$id.epub",
|
||||
timestamp = 1L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import com.aryan.reader.shared.ReaderFeatureSurface
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopStartupTest {
|
||||
@Test
|
||||
fun `startup splash uses compact branded feedback`() {
|
||||
val spec = epistemeDesktopStartupSplashSpec(desktopBuildProfileForFlavor("standard"))
|
||||
|
||||
assertEquals(EpistemeDesktopWindowTitle, spec.title)
|
||||
assertTrue(spec.message.isNotBlank())
|
||||
assertTrue(spec.width in 320..480)
|
||||
assertTrue(spec.height in 180..280)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `oss startup splash uses oss branding`() {
|
||||
val spec = epistemeDesktopStartupSplashSpec(desktopBuildProfileForFlavor("oss-offline"))
|
||||
|
||||
assertEquals(EpistemeDesktopOssAppName, spec.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `embedded webview starts only for epub backed reader surfaces`() {
|
||||
assertTrue(shouldRequestDesktopWebViewRuntime(ReaderFeatureSurface.EPUB_READER))
|
||||
assertTrue(shouldRequestDesktopWebViewRuntime(ReaderFeatureSurface.TEXT_READER))
|
||||
assertFalse(shouldRequestDesktopWebViewRuntime(ReaderFeatureSurface.PDF_VIEWER))
|
||||
assertFalse(shouldRequestDesktopWebViewRuntime(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `embedded webview startup skips terminal runtime states`() {
|
||||
assertFalse(shouldStartDesktopWebViewRuntime(requested = false, state = DesktopWebViewRuntimeState()))
|
||||
assertTrue(shouldStartDesktopWebViewRuntime(requested = true, state = DesktopWebViewRuntimeState()))
|
||||
assertFalse(
|
||||
shouldStartDesktopWebViewRuntime(
|
||||
requested = true,
|
||||
state = DesktopWebViewRuntimeState(initialized = true)
|
||||
)
|
||||
)
|
||||
assertFalse(
|
||||
shouldStartDesktopWebViewRuntime(
|
||||
requested = true,
|
||||
state = DesktopWebViewRuntimeState(restartRequired = true)
|
||||
)
|
||||
)
|
||||
assertFalse(
|
||||
shouldStartDesktopWebViewRuntime(
|
||||
requested = true,
|
||||
state = DesktopWebViewRuntimeState(errorMessage = "missing bundle")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopWindowPolishTest {
|
||||
@Test
|
||||
fun `desktop window defaults use app branding and a useful first launch size`() {
|
||||
val defaults = epistemeDesktopWindowDefaults(desktopBuildProfileForFlavor("standard"))
|
||||
|
||||
assertEquals(EpistemeDesktopWindowTitle, defaults.title)
|
||||
assertEquals(EpistemeDesktopWindowIconResource, defaults.iconResourcePath)
|
||||
assertTrue(defaults.defaultSize.width.value > defaults.minimumSize.width.toFloat())
|
||||
assertTrue(defaults.defaultSize.height.value > defaults.minimumSize.height.toFloat())
|
||||
assertEquals(EpistemeDesktopWindowMinimumWidthPx, defaults.minimumSize.width)
|
||||
assertEquals(EpistemeDesktopWindowMinimumHeightPx, defaults.minimumSize.height)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `oss desktop window defaults use oss branding`() {
|
||||
val defaults = epistemeDesktopWindowDefaults(desktopBuildProfileForFlavor("oss-offline"))
|
||||
|
||||
assertEquals(EpistemeDesktopOssAppName, defaults.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop chrome colors choose dark mode from dark theme surfaces`() {
|
||||
val darkChrome = desktopWindowChromeColors(
|
||||
captionColor = Color(0xFF12140E),
|
||||
textColor = Color(0xFFE2E3D8),
|
||||
borderColor = Color(0xFF0C0F09)
|
||||
)
|
||||
|
||||
val lightChrome = desktopWindowChromeColors(
|
||||
captionColor = Color(0xFFF9FAEF),
|
||||
textColor = Color(0xFF1A1C16),
|
||||
borderColor = Color(0xFFFFFFFF)
|
||||
)
|
||||
|
||||
assertTrue(darkChrome.useDarkMode)
|
||||
assertFalse(lightChrome.useDarkMode)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.aryan.reader.desktop
|
||||
|
||||
import kotlin.io.path.createTempFile
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DesktopWindowStateStoreTest {
|
||||
|
||||
@Test
|
||||
fun `desktop window state round trips through config store`() {
|
||||
val file = createTempFile("episteme-window-state", ".json").toFile()
|
||||
val store = DesktopWindowStateStore(file)
|
||||
val snapshot = DesktopWindowStateSnapshot(
|
||||
placement = DesktopSavedWindowPlacement.FLOATING,
|
||||
widthDp = 1440f,
|
||||
heightDp = 900f,
|
||||
xDp = 120f,
|
||||
yDp = 80f
|
||||
)
|
||||
|
||||
store.save(snapshot)
|
||||
|
||||
assertEquals(snapshot, store.load())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `desktop window state clamps too small saved bounds`() {
|
||||
val snapshot = DesktopWindowStateSnapshot(
|
||||
placement = DesktopSavedWindowPlacement.FLOATING,
|
||||
widthDp = 12f,
|
||||
heightDp = 34f
|
||||
).sanitized()
|
||||
|
||||
assertEquals(EpistemeDesktopWindowMinimumWidthPx.toFloat(), snapshot.widthDp)
|
||||
assertEquals(EpistemeDesktopWindowMinimumHeightPx.toFloat(), snapshot.heightDp)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue