diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 6ce2e2e..44f111b 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -80,6 +80,7 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
+ signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
diff --git a/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt b/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt
index 5cbac7b..7dff5e3 100644
--- a/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt
+++ b/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt
@@ -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) {
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)
+ }
}
}
diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
index bda706c..66ffb4b 100644
--- a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
+++ b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
@@ -1,30 +1,65 @@
+
+
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
index 3c40f44..c2ab5ef 100644
--- a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
+++ b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
@@ -1,170 +1,10 @@
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ android:fillColor="#0d2535"
+ android:pathData="M0,0h108v108h-108z"/>
+
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
index a571e60..f69e95f 100644
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
index 61da551..f69e95f 100644
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
index c41dd28..2257963 100644
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
index db5080a..2257963 100644
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
index 6dba46d..7b53c00 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
index da31a87..7b53c00 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
index 15ac681..568881a 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
index b216f2d..568881a 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
index f25a419..e0eac22 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
index e96783c..e0eac22 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt
index 0b834ce..2fe6352 100644
--- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt
+++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt
@@ -118,7 +118,7 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
)
is AppState.Reader -> {
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.Error -> AppState.LoadingLibrary
diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt
index 490c9d3..f670fff 100644
--- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt
+++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt
@@ -116,6 +116,8 @@ expect suspend fun loadLibraryReadingPosition(fileId: String): 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 shareLibraryBookFile(fileId: String): Boolean
diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt
index 0e942aa..722a2e8 100644
--- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt
+++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt
@@ -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() {
settingsMenuOpen = false
scope.launch {
@@ -320,7 +342,13 @@ internal fun LibraryScreen(
readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem)
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)
AppState.Reader(
fileId = item.fileId,
@@ -342,12 +370,7 @@ internal fun LibraryScreen(
scope.launch {
busy = true
try {
- if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READ)) {
- message = "Marked ${item.title} as read."
- refresh()
- } else {
- message = "Could not update ${item.title}."
- }
+ applyReadingStatus(item, BookReadingStatus.READ, "Marked ${item.title} as read.")
} finally {
busy = false
}
@@ -357,12 +380,7 @@ internal fun LibraryScreen(
scope.launch {
busy = true
try {
- if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) {
- message = "Marked ${item.title} as unread."
- refresh()
- } else {
- message = "Could not update ${item.title}."
- }
+ applyReadingStatus(item, BookReadingStatus.NEW, "Marked ${item.title} as unread.")
} finally {
busy = false
}
@@ -372,12 +390,7 @@ internal fun LibraryScreen(
scope.launch {
busy = true
try {
- if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) {
- message = "Removed marks from ${item.title}."
- refresh()
- } else {
- message = "Could not update ${item.title}."
- }
+ applyReadingStatus(item, BookReadingStatus.NEW, "Removed marks from ${item.title}.")
} finally {
busy = false
}
@@ -387,12 +400,7 @@ internal fun LibraryScreen(
scope.launch {
busy = true
try {
- if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NOT_INTERESTED)) {
- message = "Marked ${item.title} as not interested."
- refresh()
- } else {
- message = "Could not update ${item.title}."
- }
+ applyReadingStatus(item, BookReadingStatus.NOT_INTERESTED, "Marked ${item.title} as not interested.")
} finally {
busy = false
}
@@ -438,7 +446,8 @@ internal fun LibraryScreen(
librarySection(
key = "reading",
title = "reading now",
- count = readingNow.size,
+ itemCount = readingNow.size,
+ displayCount = readingNow.size.takeIf { endReached },
collapsed = readingNowCollapsed,
onCollapsedChange = { readingNowCollapsed = it },
) {
@@ -447,7 +456,8 @@ internal fun LibraryScreen(
librarySection(
key = "library",
title = "my library",
- count = myLibrary.size,
+ itemCount = myLibrary.size,
+ displayCount = myLibrary.size.takeIf { endReached },
collapsed = myLibraryCollapsed,
onCollapsedChange = { myLibraryCollapsed = it },
) {
@@ -456,7 +466,8 @@ internal fun LibraryScreen(
librarySection(
key = "not-interested",
title = "not interested",
- count = notInterested.size,
+ itemCount = notInterested.size,
+ displayCount = notInterested.size.takeIf { endReached },
collapsed = notInterestedCollapsed,
onCollapsedChange = { notInterestedCollapsed = it },
) {
@@ -573,16 +584,17 @@ private fun EmptySearchPane(modifier: Modifier = Modifier) {
private fun LazyListScope.librarySection(
key: String,
title: String,
- count: Int,
+ itemCount: Int,
+ displayCount: Int?,
collapsed: Boolean,
onCollapsedChange: (Boolean) -> Unit,
content: LazyListScope.() -> Unit,
) {
- if (count == 0) return
+ if (itemCount == 0) return
item(key = "section-$key") {
LibrarySectionHeader(
text = title,
- count = count,
+ count = displayCount,
collapsed = collapsed,
onToggle = { onCollapsedChange(!collapsed) },
)
@@ -595,7 +607,7 @@ private fun LazyListScope.librarySection(
@Composable
private fun LibrarySectionHeader(
text: String,
- count: Int,
+ count: Int?,
collapsed: Boolean,
onToggle: () -> Unit,
) {
@@ -614,7 +626,7 @@ private fun LibrarySectionHeader(
modifier = Modifier.size(18.dp),
)
Text(
- "$text ($count)",
+ if (count == null) text else "$text ($count)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
textAlign = TextAlign.Center,
diff --git a/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt b/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt
index 305d413..52c3822 100644
--- a/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt
+++ b/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt
@@ -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) {
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)
+ }
}
}
diff --git a/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt b/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt
index 12c0f4a..6386843 100644
--- a/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt
+++ b/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt
@@ -49,6 +49,8 @@ actual suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition?
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 shareLibraryBookFile(fileId: String): Boolean = false
diff --git a/image_src/book.svg b/image_src/book.svg
new file mode 100644
index 0000000..95a8f62
--- /dev/null
+++ b/image_src/book.svg
@@ -0,0 +1,83 @@
+
+
diff --git a/image_src/favicon.svg b/image_src/favicon.svg
new file mode 100644
index 0000000..dc3ba27
--- /dev/null
+++ b/image_src/favicon.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/image_src/icon-app.svg b/image_src/icon-app.svg
new file mode 100644
index 0000000..96896f3
--- /dev/null
+++ b/image_src/icon-app.svg
@@ -0,0 +1,65 @@
+
+
+
diff --git a/image_src/playstore-feature.svg b/image_src/playstore-feature.svg
new file mode 100644
index 0000000..d96b967
--- /dev/null
+++ b/image_src/playstore-feature.svg
@@ -0,0 +1,108 @@
+
+
+
diff --git a/image_src/star-colored.svg b/image_src/star-colored.svg
new file mode 100644
index 0000000..338ca23
--- /dev/null
+++ b/image_src/star-colored.svg
@@ -0,0 +1,51 @@
+
+
diff --git a/shared/src/commonMain/kotlin/net/sergeych/toread/storage/LibraryStorage.kt b/shared/src/commonMain/kotlin/net/sergeych/toread/storage/LibraryStorage.kt
index 1a0ab48..e6de8ff 100644
--- a/shared/src/commonMain/kotlin/net/sergeych/toread/storage/LibraryStorage.kt
+++ b/shared/src/commonMain/kotlin/net/sergeych/toread/storage/LibraryStorage.kt
@@ -217,6 +217,8 @@ interface ReadingStateRepository {
fun upsert(state: ReadingStateRecord)
fun get(id: String): ReadingStateRecord?
fun getForBodyCluster(bodyClusterId: String): ReadingStateRecord?
+ fun deleteForBodyCluster(bodyClusterId: String): Int
+ fun delete(id: String): Boolean
}
interface BookmarkRepository {
diff --git a/shared/src/jdbcMain/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabase.kt b/shared/src/jdbcMain/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabase.kt
index a1f168f..f9c12fb 100644
--- a/shared/src/jdbcMain/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabase.kt
+++ b/shared/src/jdbcMain/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabase.kt
@@ -862,6 +862,15 @@ private class JdbcReadingStateRepository(private val connection: Connection) : R
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 {
diff --git a/shared/src/jvmTest/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabaseTest.kt b/shared/src/jvmTest/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabaseTest.kt
index bbaca71..e500d7b 100644
--- a/shared/src/jvmTest/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabaseTest.kt
+++ b/shared/src/jvmTest/kotlin/net/sergeych/toread/storage/jdbc/H2LibraryDatabaseTest.kt
@@ -376,6 +376,42 @@ class H2LibraryDatabaseTest {
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
fun migratesLegacyBookFilesWithImportTime() {
val path = Files.createTempDirectory("toread-h2-imported-at-").resolve("library").toString()