many improvements and prototype launcher image

This commit is contained in:
Sergey Chernov 2026-05-18 02:09:15 +03:00
parent 11c8f7c6e4
commit 6422f814d1
27 changed files with 530 additions and 232 deletions

View File

@ -80,6 +80,7 @@ android {
buildTypes { buildTypes {
getByName("release") { getByName("release") {
isMinifyEnabled = false isMinifyEnabled = false
signingConfig = signingConfigs.getByName("debug")
} }
} }
compileOptions { compileOptions {

View File

@ -307,9 +307,24 @@ actual suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingP
} }
} }
actual suspend fun clearLibraryReadingPosition(fileId: String) = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
val file = db.files.get(fileId) ?: return@useLibrary
val clusterId = file.bodyClusterId ?: return@useLibrary
db.readingStates.deleteForBodyCluster(clusterId)
}
Unit
}
actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = withContext(Dispatchers.IO) { actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db -> openLibraryDatabase().useLibrary { db ->
db.files.updateReadingStatus(fileId, status) db.transaction {
if (status == BookReadingStatus.NEW) {
val file = files.get(fileId) ?: return@transaction false
file.bodyClusterId?.let { readingStates.deleteForBodyCluster(it) }
}
files.updateReadingStatus(fileId, status)
}
} }
} }

View File

@ -1,30 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Adaptive icon foreground: 108dp x 108dp canvas.
Safe zone: inner 72dp circle (18dp..90dp). Book (200x260) scaled 0.2769x:
translateX=26.31, translateY=18. Star sub-group at book coords (67,59) scale 0.888x.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor"> <group
<gradient android:translateX="26.31"
android:endX="85.84757" android:translateY="18"
android:endY="92.4963" android:scaleX="0.2769"
android:startX="42.9492" android:scaleY="0.2769">
android:startY="49.59793"
android:type="linear"> <!-- Pages stack -->
<item <path android:fillColor="#e8e4d8"
android:color="#44000000" android:pathData="M36,18 H178 V248 H36 Z"/>
android:offset="0.0"/> <path android:fillColor="#ece9df"
<item android:pathData="M34,16 H176 V246 H34 Z"/>
android:color="#00000000" <path android:fillColor="#f0ede3"
android:offset="1.0"/> android:pathData="M32,14 H174 V244 H32 Z"/>
</gradient>
</aapt:attr> <!-- Spine -->
</path> <path android:fillColor="#1a3d52"
<path android:pathData="M18,14 H36 V244 H18 Z"/>
android:fillColor="#FFFFFF" <path android:fillColor="#00000000"
android:fillType="nonZero" android:strokeColor="#30000000"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1" android:strokeWidth="1"
android:strokeColor="#00000000"/> android:pathData="M35,14 V244"/>
<!-- Cover -->
<path android:fillColor="#2c5f7a"
android:pathData="M32,14 H174 V244 H32 Z"/>
<!-- Cover border -->
<path android:fillColor="#00000000"
android:strokeColor="#30ffffff"
android:strokeWidth="1.5"
android:pathData="M40,22 H166 V236 H40 Z"/>
<!-- Star arms -->
<group
android:translateX="67"
android:translateY="59"
android:scaleX="0.888"
android:scaleY="0.888">
<path android:fillColor="#6197b4"
android:pathData="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z"/>
<path android:fillColor="#bdccd6"
android:pathData="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z"/>
<path android:fillColor="#94bfcf"
android:pathData="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z"/>
<path android:fillColor="#eee9d9"
android:pathData="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z"/>
</group>
<!-- Bottom spine accent -->
<path android:fillColor="#0e2030"
android:pathData="M18,228 H36 V244 H18 Z"/>
</group>
</vector> </vector>

View File

@ -5,166 +5,6 @@
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<path <path
android:fillColor="#3DDC84" android:fillColor="#0d2535"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
</vector> </vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -118,7 +118,7 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
) )
is AppState.Reader -> { is AppState.Reader -> {
scope.launch { saveActiveReadingFileId(null) } scope.launch { saveActiveReadingFileId(null) }
AppState.Library(current.libraryItems, current.scanPath, current.message) AppState.Library(emptyList(), current.scanPath, current.message)
} }
is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message) is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message)
is AppState.Error -> AppState.LoadingLibrary is AppState.Error -> AppState.LoadingLibrary

View File

@ -116,6 +116,8 @@ expect suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition?
expect suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingPosition) expect suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingPosition)
expect suspend fun clearLibraryReadingPosition(fileId: String)
expect suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean expect suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean
expect suspend fun shareLibraryBookFile(fileId: String): Boolean expect suspend fun shareLibraryBookFile(fileId: String): Boolean

View File

@ -146,6 +146,28 @@ internal fun LibraryScreen(
} }
} }
suspend fun applyReadingStatus(
item: LibraryItem,
status: BookReadingStatus,
successMessage: String,
) {
if (markLibraryReadingStatus(item.fileId, status)) {
val updatedItem = loadLibraryItem(item.fileId) ?: item.copy(readingStatus = status)
items = items.replaceLibraryItem(updatedItem)
searchResults = searchResults.replaceLibraryItem(updatedItem)
message = successMessage
if (searchActive) {
searching = true
searchResults = searchLibraryItems(searchText, SearchResultLimit)
searching = false
} else {
loadPage(reset = true)
}
} else {
message = "Could not update ${item.title}."
}
}
fun rescanAllLibrary() { fun rescanAllLibrary() {
settingsMenuOpen = false settingsMenuOpen = false
scope.launch { scope.launch {
@ -320,7 +342,13 @@ internal fun LibraryScreen(
readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem) readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem)
coverCache[updatedItem.fileId] = loadLibraryItemCover(updatedItem.fileId) coverCache[updatedItem.fileId] = loadLibraryItemCover(updatedItem.fileId)
} }
markLibraryReadingStatus(item.fileId, BookReadingStatus.READING) if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)) {
val readingItem = loadLibraryItem(item.fileId)
?: item.copy(readingStatus = BookReadingStatus.READING)
items = items.replaceLibraryItem(readingItem)
searchResults = searchResults.replaceLibraryItem(readingItem)
readerLibraryItems = readerLibraryItems.replaceLibraryItem(readingItem)
}
saveActiveReadingFileId(item.fileId) saveActiveReadingFileId(item.fileId)
AppState.Reader( AppState.Reader(
fileId = item.fileId, fileId = item.fileId,
@ -342,12 +370,7 @@ internal fun LibraryScreen(
scope.launch { scope.launch {
busy = true busy = true
try { try {
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READ)) { applyReadingStatus(item, BookReadingStatus.READ, "Marked ${item.title} as read.")
message = "Marked ${item.title} as read."
refresh()
} else {
message = "Could not update ${item.title}."
}
} finally { } finally {
busy = false busy = false
} }
@ -357,12 +380,7 @@ internal fun LibraryScreen(
scope.launch { scope.launch {
busy = true busy = true
try { try {
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) { applyReadingStatus(item, BookReadingStatus.NEW, "Marked ${item.title} as unread.")
message = "Marked ${item.title} as unread."
refresh()
} else {
message = "Could not update ${item.title}."
}
} finally { } finally {
busy = false busy = false
} }
@ -372,12 +390,7 @@ internal fun LibraryScreen(
scope.launch { scope.launch {
busy = true busy = true
try { try {
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) { applyReadingStatus(item, BookReadingStatus.NEW, "Removed marks from ${item.title}.")
message = "Removed marks from ${item.title}."
refresh()
} else {
message = "Could not update ${item.title}."
}
} finally { } finally {
busy = false busy = false
} }
@ -387,12 +400,7 @@ internal fun LibraryScreen(
scope.launch { scope.launch {
busy = true busy = true
try { try {
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NOT_INTERESTED)) { applyReadingStatus(item, BookReadingStatus.NOT_INTERESTED, "Marked ${item.title} as not interested.")
message = "Marked ${item.title} as not interested."
refresh()
} else {
message = "Could not update ${item.title}."
}
} finally { } finally {
busy = false busy = false
} }
@ -438,7 +446,8 @@ internal fun LibraryScreen(
librarySection( librarySection(
key = "reading", key = "reading",
title = "reading now", title = "reading now",
count = readingNow.size, itemCount = readingNow.size,
displayCount = readingNow.size.takeIf { endReached },
collapsed = readingNowCollapsed, collapsed = readingNowCollapsed,
onCollapsedChange = { readingNowCollapsed = it }, onCollapsedChange = { readingNowCollapsed = it },
) { ) {
@ -447,7 +456,8 @@ internal fun LibraryScreen(
librarySection( librarySection(
key = "library", key = "library",
title = "my library", title = "my library",
count = myLibrary.size, itemCount = myLibrary.size,
displayCount = myLibrary.size.takeIf { endReached },
collapsed = myLibraryCollapsed, collapsed = myLibraryCollapsed,
onCollapsedChange = { myLibraryCollapsed = it }, onCollapsedChange = { myLibraryCollapsed = it },
) { ) {
@ -456,7 +466,8 @@ internal fun LibraryScreen(
librarySection( librarySection(
key = "not-interested", key = "not-interested",
title = "not interested", title = "not interested",
count = notInterested.size, itemCount = notInterested.size,
displayCount = notInterested.size.takeIf { endReached },
collapsed = notInterestedCollapsed, collapsed = notInterestedCollapsed,
onCollapsedChange = { notInterestedCollapsed = it }, onCollapsedChange = { notInterestedCollapsed = it },
) { ) {
@ -573,16 +584,17 @@ private fun EmptySearchPane(modifier: Modifier = Modifier) {
private fun LazyListScope.librarySection( private fun LazyListScope.librarySection(
key: String, key: String,
title: String, title: String,
count: Int, itemCount: Int,
displayCount: Int?,
collapsed: Boolean, collapsed: Boolean,
onCollapsedChange: (Boolean) -> Unit, onCollapsedChange: (Boolean) -> Unit,
content: LazyListScope.() -> Unit, content: LazyListScope.() -> Unit,
) { ) {
if (count == 0) return if (itemCount == 0) return
item(key = "section-$key") { item(key = "section-$key") {
LibrarySectionHeader( LibrarySectionHeader(
text = title, text = title,
count = count, count = displayCount,
collapsed = collapsed, collapsed = collapsed,
onToggle = { onCollapsedChange(!collapsed) }, onToggle = { onCollapsedChange(!collapsed) },
) )
@ -595,7 +607,7 @@ private fun LazyListScope.librarySection(
@Composable @Composable
private fun LibrarySectionHeader( private fun LibrarySectionHeader(
text: String, text: String,
count: Int, count: Int?,
collapsed: Boolean, collapsed: Boolean,
onToggle: () -> Unit, onToggle: () -> Unit,
) { ) {
@ -614,7 +626,7 @@ private fun LibrarySectionHeader(
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
) )
Text( Text(
"$text ($count)", if (count == null) text else "$text ($count)",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View File

@ -255,9 +255,24 @@ actual suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingP
} }
} }
actual suspend fun clearLibraryReadingPosition(fileId: String) = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
val file = db.files.get(fileId) ?: return@useLibrary
val clusterId = file.bodyClusterId ?: return@useLibrary
db.readingStates.deleteForBodyCluster(clusterId)
}
Unit
}
actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = withContext(Dispatchers.IO) { actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db -> openLibraryDatabase().useLibrary { db ->
db.files.updateReadingStatus(fileId, status) db.transaction {
if (status == BookReadingStatus.NEW) {
val file = files.get(fileId) ?: return@transaction false
file.bodyClusterId?.let { readingStates.deleteForBodyCluster(it) }
}
files.updateReadingStatus(fileId, status)
}
} }
} }

View File

@ -49,6 +49,8 @@ actual suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition?
actual suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingPosition) = Unit actual suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingPosition) = Unit
actual suspend fun clearLibraryReadingPosition(fileId: String) = Unit
actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = false actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = false
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false

83
image_src/book.svg Normal file
View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="200"
height="260"
viewBox="0 0 200 260"
fill="none"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Drop shadow filter -->
<filter id="shadow" x="-10%" y="-5%" width="130%" height="120%">
<feDropShadow dx="4" dy="4" stdDeviation="5" flood-color="#00000044"/>
</filter>
<!-- Page texture gradient -->
<linearGradient id="pageGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f0ede3"/>
<stop offset="100%" stop-color="#e8e4d8"/>
</linearGradient>
<!-- Cover gradient -->
<linearGradient id="coverGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#2c5f7a"/>
<stop offset="100%" stop-color="#1a3d52"/>
</linearGradient>
<!-- Spine gradient -->
<linearGradient id="spineGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#1a3d52"/>
<stop offset="100%" stop-color="#2c5f7a"/>
</linearGradient>
<!-- Highlight on cover -->
<linearGradient id="coverShine" x1="0%" y1="0%" x2="30%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.12"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Book pages (stacked, slightly offset) -->
<rect x="36" y="18" width="142" height="230" rx="2" fill="url(#pageGrad)" filter="url(#shadow)"/>
<rect x="34" y="16" width="142" height="230" rx="2" fill="#ece9df"/>
<rect x="32" y="14" width="142" height="230" rx="2" fill="#f0ede3"/>
<!-- Book spine -->
<rect x="18" y="14" width="18" height="230" rx="2" fill="url(#spineGrad)"/>
<!-- Spine inner shadow line -->
<line x1="35" y1="14" x2="35" y2="244" stroke="#00000030" stroke-width="1"/>
<!-- Book cover (front) -->
<rect x="32" y="14" width="142" height="230" rx="2" fill="url(#coverGrad)"/>
<!-- Cover shine overlay -->
<rect x="32" y="14" width="142" height="230" rx="2" fill="url(#coverShine)"/>
<!-- Cover border decoration -->
<rect x="40" y="22" width="126" height="214" rx="1" fill="none" stroke="#ffffff30" stroke-width="1.5"/>
<!-- Star graphic centered on cover (scaled & translated star-colored.svg paths) -->
<!-- Original star viewBox: 0 0 81 81, scaled to ~72x72, centered at (103, 95) -->
<g transform="translate(67, 59) scale(0.888)">
<!-- path9: blue-gray arm -->
<path
d="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z"
fill="#6197b4"/>
<!-- path11: light arm -->
<path
d="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z"
fill="#bdccd6"/>
<!-- path13: medium arm -->
<path
d="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z"
fill="#94bfcf"/>
<!-- path15: cream arm -->
<path
d="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z"
fill="#eee9d9"/>
</g>
<!-- Title text -->
<text x="103" y="180" text-anchor="middle" font-family="Georgia, serif" font-size="15" font-weight="bold" fill="#ffffff" letter-spacing="1">TOREAD</text>
<!-- Subtitle / decorative line -->
<line x1="55" y1="188" x2="151" y2="188" stroke="#ffffff50" stroke-width="0.8"/>
<text x="103" y="202" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#aaccdd" letter-spacing="2">E-READER</text>
<!-- Bottom spine detail -->
<rect x="18" y="228" width="18" height="16" rx="0 0 2 2" fill="#163348"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

22
image_src/favicon.svg Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 32x32 web favicon — star symbol on teal rounded background -->
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="fg_bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3a82a6"/>
<stop offset="100%" stop-color="#1a3d52"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="32" height="32" rx="6" fill="url(#fg_bg)"/>
<!-- Star from star-colored.svg, scaled to 24x24, centered (4px padding) -->
<!-- Original viewBox 81x81 → scale=24/81=0.2963, translate(4,4) -->
<g transform="translate(4, 4) scale(0.2963)">
<path d="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z" fill="#6197b4"/>
<path d="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z" fill="#bdccd6"/>
<path d="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z" fill="#94bfcf"/>
<path d="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z" fill="#eee9d9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

65
image_src/icon-app.svg Normal file
View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 512x512 app icon — desktop launcher & Android (keep content in inner ~340px safe zone) -->
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="bgGrad" cx="45%" cy="35%" r="75%">
<stop offset="0%" stop-color="#2e6b8a"/>
<stop offset="100%" stop-color="#0d2535"/>
</radialGradient>
<filter id="shadow" x="-10%" y="-5%" width="130%" height="120%">
<feDropShadow dx="5" dy="7" stdDeviation="8" flood-color="#00000066"/>
</filter>
<linearGradient id="pageGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f0ede3"/>
<stop offset="100%" stop-color="#e8e4d8"/>
</linearGradient>
<linearGradient id="coverGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3a82a6"/>
<stop offset="100%" stop-color="#1a3d52"/>
</linearGradient>
<linearGradient id="spineGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#122c3c"/>
<stop offset="100%" stop-color="#2e6b8a"/>
</linearGradient>
<linearGradient id="coverShine" x1="0%" y1="0%" x2="30%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Full-bleed background -->
<rect width="512" height="512" fill="url(#bgGrad)"/>
<!-- Subtle radial glow behind book -->
<ellipse cx="268" cy="256" rx="200" ry="220" fill="#2e6b8a" opacity="0.25"/>
<!-- Book, scaled 1.84x and centered (book canvas 200x260 → 368x478, centered in 512x512) -->
<!-- x_offset=(512-368)/2=72, y_offset=(512-478)/2=17 -->
<g transform="translate(72, 17) scale(1.84)">
<!-- Pages -->
<rect x="36" y="18" width="142" height="230" rx="2" fill="url(#pageGrad)" filter="url(#shadow)"/>
<rect x="34" y="16" width="142" height="230" rx="2" fill="#ece9df"/>
<rect x="32" y="14" width="142" height="230" rx="2" fill="#f0ede3"/>
<!-- Spine -->
<rect x="18" y="14" width="18" height="230" rx="2" fill="url(#spineGrad)"/>
<line x1="35" y1="14" x2="35" y2="244" stroke="#00000030" stroke-width="1"/>
<!-- Cover -->
<rect x="32" y="14" width="142" height="230" rx="2" fill="url(#coverGrad)"/>
<rect x="32" y="14" width="142" height="230" rx="2" fill="url(#coverShine)"/>
<!-- Cover border -->
<rect x="40" y="22" width="126" height="214" rx="1" fill="none" stroke="#ffffff30" stroke-width="1.5"/>
<!-- Star (scale 0.888 within book, translate 67,59) -->
<g transform="translate(67, 59) scale(0.888)">
<path d="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z" fill="#6197b4"/>
<path d="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z" fill="#bdccd6"/>
<path d="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z" fill="#94bfcf"/>
<path d="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z" fill="#eee9d9"/>
</g>
<!-- Title -->
<text x="103" y="180" text-anchor="middle" font-family="Georgia, serif" font-size="15" font-weight="bold" fill="#ffffff" letter-spacing="1">TOREAD</text>
<line x1="55" y1="188" x2="151" y2="188" stroke="#ffffff50" stroke-width="0.8"/>
<text x="103" y="202" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#aaccdd" letter-spacing="2">E-READER</text>
<!-- Bottom spine detail -->
<rect x="18" y="228" width="18" height="16" fill="#0e2030"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Play Store feature graphic: 1024x500 -->
<svg width="1024" height="500" viewBox="0 0 1024 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Background gradient -->
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0d2535"/>
<stop offset="60%" stop-color="#1a4a66"/>
<stop offset="100%" stop-color="#0a1e2c"/>
</linearGradient>
<!-- Accent glow -->
<radialGradient id="glow" cx="72%" cy="50%" r="40%">
<stop offset="0%" stop-color="#3a82a6" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#3a82a6" stop-opacity="0"/>
</radialGradient>
<!-- Book defs -->
<filter id="bookshadow" x="-15%" y="-10%" width="140%" height="130%">
<feDropShadow dx="8" dy="12" stdDeviation="14" flood-color="#00000088"/>
</filter>
<linearGradient id="pageGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f0ede3"/>
<stop offset="100%" stop-color="#e8e4d8"/>
</linearGradient>
<linearGradient id="coverGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3a82a6"/>
<stop offset="100%" stop-color="#1a3d52"/>
</linearGradient>
<linearGradient id="spineGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#122c3c"/>
<stop offset="100%" stop-color="#2e6b8a"/>
</linearGradient>
<linearGradient id="coverShine" x1="0%" y1="0%" x2="30%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<!-- Subtle divider gradient -->
<linearGradient id="divider" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#aaccdd" stop-opacity="0"/>
<stop offset="50%" stop-color="#aaccdd" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#aaccdd" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1024" height="500" fill="url(#bg)"/>
<!-- Radial glow on right where book sits -->
<rect width="1024" height="500" fill="url(#glow)"/>
<!-- Decorative circles (faint) -->
<circle cx="780" cy="250" r="280" fill="none" stroke="#3a82a6" stroke-width="0.6" opacity="0.15"/>
<circle cx="780" cy="250" r="210" fill="none" stroke="#3a82a6" stroke-width="0.6" opacity="0.15"/>
<circle cx="780" cy="250" r="140" fill="none" stroke="#3a82a6" stroke-width="0.6" opacity="0.15"/>
<!-- Left panel: branding -->
<!-- Large star symbol, centered left area -->
<!-- star viewBox 81x81, scale to 110x110 at position (70, 100) -->
<!-- scale=110/81=1.358, translate(70,100) -->
<g transform="translate(70, 120) scale(1.358)" opacity="0.85">
<path d="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z" fill="#6197b4"/>
<path d="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z" fill="#bdccd6"/>
<path d="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z" fill="#94bfcf"/>
<path d="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z" fill="#eee9d9"/>
</g>
<!-- App name -->
<text x="215" y="175" font-family="Georgia, serif" font-size="88" font-weight="bold" fill="#ffffff" letter-spacing="4">TOREAD</text>
<!-- Tagline -->
<text x="218" y="224" font-family="Georgia, serif" font-size="26" fill="#aaccdd" letter-spacing="3">Your e-reading companion</text>
<!-- Feature bullets -->
<text x="218" y="282" font-family="Georgia, serif" font-size="18" fill="#7baabf">· EPUB &amp; FB2 support</text>
<text x="218" y="312" font-family="Georgia, serif" font-size="18" fill="#7baabf">· Clean reader, smart library</text>
<text x="218" y="342" font-family="Georgia, serif" font-size="18" fill="#7baabf">· Read aloud &amp; offline</text>
<!-- Vertical divider line -->
<line x1="600" y1="40" x2="600" y2="460" stroke="url(#divider)" stroke-width="1"/>
<!-- Book on the right, scale to fit 420px height -->
<!-- scale=420/260=1.615, width=200*1.615=323 -->
<!-- x: 600 + (424-323)/2 = 600+50 = 650, y: (500-420)/2=40 -->
<g transform="translate(652, 38) scale(1.615)">
<!-- Pages -->
<rect x="36" y="18" width="142" height="230" rx="2" fill="url(#pageGrad)" filter="url(#bookshadow)"/>
<rect x="34" y="16" width="142" height="230" rx="2" fill="#ece9df"/>
<rect x="32" y="14" width="142" height="230" rx="2" fill="#f0ede3"/>
<!-- Spine -->
<rect x="18" y="14" width="18" height="230" rx="2" fill="url(#spineGrad)"/>
<line x1="35" y1="14" x2="35" y2="244" stroke="#00000030" stroke-width="1"/>
<!-- Cover -->
<rect x="32" y="14" width="142" height="230" rx="2" fill="url(#coverGrad)"/>
<rect x="32" y="14" width="142" height="230" rx="2" fill="url(#coverShine)"/>
<!-- Cover border -->
<rect x="40" y="22" width="126" height="214" rx="1" fill="none" stroke="#ffffff30" stroke-width="1.5"/>
<!-- Star on cover -->
<g transform="translate(67, 59) scale(0.888)">
<path d="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z" fill="#6197b4"/>
<path d="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z" fill="#bdccd6"/>
<path d="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z" fill="#94bfcf"/>
<path d="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z" fill="#eee9d9"/>
</g>
<!-- Title -->
<text x="103" y="180" text-anchor="middle" font-family="Georgia, serif" font-size="15" font-weight="bold" fill="#ffffff" letter-spacing="1">TOREAD</text>
<line x1="55" y1="188" x2="151" y2="188" stroke="#ffffff50" stroke-width="0.8"/>
<text x="103" y="202" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#aaccdd" letter-spacing="2">E-READER</text>
<rect x="18" y="228" width="18" height="16" fill="#0e2030"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="81.0028"
height="81.178841"
viewBox="0 0 81.0028 81.178841"
fill="none"
version="1.1"
id="svg17"
sodipodi:docname="star-colored.svg"
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs21" />
<sodipodi:namedview
id="namedview19"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="6.7100498"
inkscape:cx="56.780502"
inkscape:cy="58.047259"
inkscape:window-width="1312"
inkscape:window-height="1067"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg17" />
<path
d="m 32.120065,25.957965 -15.0296,-9.2912 c -0.3066,-0.187 -0.6532,0.1603 -0.4666,0.4676 l 8.9112,14.4745 c 0.2399,0.3874 0.5665,0.7013 0.9597,0.9284 l 14.0032,8.0555 V 0.34209525 c 0,-0.394094 -0.5532,-0.474248 -0.6665,-0.1002 z"
fill="#6197b4"
id="path9" />
<path
d="m 25.901665,48.988865 -9.271,15.0556 c -0.1866,0.3073 0.16,0.6546 0.4665,0.4676 l 14.4431,-8.9305 c 0.3866,-0.2405 0.6999,-0.5678 0.9265,-0.9619 l 8.038,-14.0336 H 0.34134532 c -0.393231,0 -0.473212,0.5544 -0.09997,0.6679 z"
fill="#bdccd6"
id="path11" />
<path
d="m 55.101065,32.189965 9.2711,-15.0623 c 0.1866,-0.3072 -0.16,-0.6546 -0.4666,-0.4675 l -14.4431,8.9371 c -0.3865,0.2405 -0.6998,0.5678 -0.9264,0.9619 l -8.038,14.027 h 40.1634 c 0.3932,0 0.4732,-0.5544 0.1,-0.668 z"
fill="#94bfcf"
id="path13" />
<path
d="m 54.507965,48.641565 -14.0099,-8.0555 v 40.2507 c 0,0.3941 0.5532,0.4742 0.6665,0.1002 l 7.7181,-25.7094 15.0296,9.2912 c 0.3066,0.187 0.6532,-0.1603 0.4665,-0.4676 l -8.9177,-14.4812 c -0.2333,-0.3807 -0.5666,-0.7013 -0.9531,-0.9284 z"
fill="#eee9d9"
id="path15" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -217,6 +217,8 @@ interface ReadingStateRepository {
fun upsert(state: ReadingStateRecord) fun upsert(state: ReadingStateRecord)
fun get(id: String): ReadingStateRecord? fun get(id: String): ReadingStateRecord?
fun getForBodyCluster(bodyClusterId: String): ReadingStateRecord? fun getForBodyCluster(bodyClusterId: String): ReadingStateRecord?
fun deleteForBodyCluster(bodyClusterId: String): Int
fun delete(id: String): Boolean
} }
interface BookmarkRepository { interface BookmarkRepository {

View File

@ -862,6 +862,15 @@ private class JdbcReadingStateRepository(private val connection: Connection) : R
it.toReadingStateRecord() it.toReadingStateRecord()
} }
} }
override fun deleteForBodyCluster(bodyClusterId: String): Int {
return connection.prepareStatement("DELETE FROM reading_states WHERE body_cluster_id = ?").use { statement ->
statement.setString(1, bodyClusterId)
statement.executeUpdate()
}
}
override fun delete(id: String): Boolean = connection.deleteById("reading_states", id)
} }
private class JdbcBookmarkRepository(private val connection: Connection) : BookmarkRepository { private class JdbcBookmarkRepository(private val connection: Connection) : BookmarkRepository {

View File

@ -376,6 +376,42 @@ class H2LibraryDatabaseTest {
H2LibraryDatabase.openFile(path).close() H2LibraryDatabase.openFile(path).close()
} }
@Test
fun persistsReadingStatusAcrossDatabaseRestart() {
val path = Files.createTempDirectory("toread-h2-reading-status-").resolve("library").toString()
val now = 1_700_000_000_000L
val initialDb = H2LibraryDatabase.openFile(path)
try {
val db = initialDb
db.transaction {
books.upsert(BookRecord(id = "book-1", title = "Persistent", createdAt = now, updatedAt = now))
files.upsert(
BookFileRecord(
id = "file-1",
bookId = "book-1",
rawSha256 = "sha-1",
storageKind = BookFileStorageKind.EXTERNAL_URI,
createdAt = now,
updatedAt = now,
)
)
}
assertEquals(true, db.files.updateReadingStatus("file-1", BookReadingStatus.NOT_INTERESTED))
} finally {
initialDb.close()
}
val reopenedDb = H2LibraryDatabase.openFile(path)
try {
val db = reopenedDb
assertEquals(BookReadingStatus.NOT_INTERESTED, db.files.get("file-1")?.readingStatus)
assertEquals(BookReadingStatus.NOT_INTERESTED, db.files.getLibraryFile("file-1")?.readingStatus)
} finally {
reopenedDb.close()
}
}
@Test @Test
fun migratesLegacyBookFilesWithImportTime() { fun migratesLegacyBookFilesWithImportTime() {
val path = Files.createTempDirectory("toread-h2-imported-at-").resolve("library").toString() val path = Files.createTempDirectory("toread-h2-imported-at-").resolve("library").toString()