diff --git a/.gitignore b/.gitignore index adfa9bf..14eaa7e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ captures !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings node_modules/ +/composeApp/release/ diff --git a/composeApp/release/baselineProfiles/0/composeApp-release.dm b/composeApp/release/baselineProfiles/0/composeApp-release.dm deleted file mode 100644 index ca52bba..0000000 Binary files a/composeApp/release/baselineProfiles/0/composeApp-release.dm and /dev/null differ diff --git a/composeApp/release/baselineProfiles/1/composeApp-release.dm b/composeApp/release/baselineProfiles/1/composeApp-release.dm deleted file mode 100644 index c996fdf..0000000 Binary files a/composeApp/release/baselineProfiles/1/composeApp-release.dm and /dev/null differ diff --git a/composeApp/release/composeApp-release.apk b/composeApp/release/composeApp-release.apk deleted file mode 100644 index 008e5a0..0000000 Binary files a/composeApp/release/composeApp-release.apk and /dev/null differ diff --git a/composeApp/release/output-metadata.json b/composeApp/release/output-metadata.json deleted file mode 100644 index 164e284..0000000 --- a/composeApp/release/output-metadata.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "version": 3, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "net.sergeych.toread", - "variantName": "release", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "attributes": [], - "versionCode": 1, - "versionName": "1.0", - "outputFile": "composeApp-release.apk" - } - ], - "elementType": "File", - "baselineProfiles": [ - { - "minApi": 28, - "maxApi": 30, - "baselineProfiles": [ - "baselineProfiles/1/composeApp-release.dm" - ] - }, - { - "minApi": 31, - "maxApi": 2147483647, - "baselineProfiles": [ - "baselineProfiles/0/composeApp-release.dm" - ] - } - ], - "minSdkVersionForDexing": 26 -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt index 13dc946..0d985b0 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt @@ -120,6 +120,7 @@ internal fun LibraryScreen( val sourceItems = if (searchActive) visibleSearchResults else libraryItems val recentlyAdded = recentlyAddedItems.filterNot { it.fileId in hiddenFileIds } val visibleItems = selectedFilter.apply(sourceItems, recentlyAdded, searchActive) + .withoutDuplicateFileIds() val canLoadMore = !searchActive && selectedFilter.usesPagedLibrary && !endReached suspend fun loadPage(reset: Boolean = false) { @@ -143,7 +144,7 @@ internal fun LibraryScreen( if (fileId !in visibleFileIds) coverCache.remove(fileId) } } else { - items = items + page + items = items.appendNewLibraryItems(page) } nextOffset = offset + page.size endReached = page.size < limit @@ -1032,13 +1033,7 @@ private enum class LibraryFilter(val usesPagedLibrary: Boolean = true) { .filter { it.readingStatus == BookReadingStatus.NEW } .sortedByImportedThenTitle() } - MyLibrary -> sourceItems.filter { - it.fileId !in recentlyAddedIds && - it.readingStatus != BookReadingStatus.READING && - it.readingStatus != BookReadingStatus.TO_READ && - it.readingStatus != BookReadingStatus.READ && - it.readingStatus != BookReadingStatus.NOT_INTERESTED - }.sortedByTitleNaturally() + MyLibrary -> sourceItems.myLibraryItems() ToRead -> sourceItems .filter { it.readingStatus == BookReadingStatus.TO_READ } .sortedByLastReadThenTitle() @@ -1070,6 +1065,10 @@ private fun List.sortedByImportedThenTitle(): List = private fun List.sortedByTitleNaturally(): List = sortedWith(LibraryItemNaturalTitleComparator) +internal fun List.myLibraryItems(): List = + filter { it.readingStatus != BookReadingStatus.NOT_INTERESTED } + .sortedByTitleNaturally() + private val LibraryItemNaturalTitleComparator = Comparator { left, right -> val titleCompare = naturalCompare(left.title, right.title) if (titleCompare != 0) titleCompare else left.fileId.compareTo(right.fileId) @@ -1194,6 +1193,15 @@ private fun Long.formatBytes(): String = private fun List.replaceLibraryItem(item: LibraryItem): List = map { current -> if (current.fileId == item.fileId) item else current } +internal fun List.appendNewLibraryItems(page: List): List { + if (isEmpty()) return page.withoutDuplicateFileIds() + val fileIds = mapTo(mutableSetOf()) { it.fileId } + return this + page.filter { fileIds.add(it.fileId) } +} + +internal fun List.withoutDuplicateFileIds(): List = + distinctBy { it.fileId } + private fun Key.isEnterKey(): Boolean = this == Key.Enter || this == Key.NumPadEnter private fun LibraryScanProgress.toCatalogScanMessage(): String { diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReadAloudPlatform.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReadAloudPlatform.kt index 16026a0..79e7c98 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReadAloudPlatform.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReadAloudPlatform.kt @@ -34,6 +34,7 @@ data class ReadAloudVoiceOption( data class ReadAloudTextReplacement( val from: String, val to: String, + val caseSensitive: Boolean = false, ) data class ReadAloudSettingsState( diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt index b0c8cbf..f184565 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt @@ -537,7 +537,7 @@ private fun ReaderText( private fun readerParagraphTextStyle(language: String?): TextStyle = MaterialTheme.typography.bodyLarge.copy( fontWeight = if( isAndroidPlatform) FontWeight(350) else FontWeight.Normal, - fontSize = if( isAndroidPlatform) 19.sp else 18.sp, + fontSize = if( isAndroidPlatform) 20.sp else 18.sp, lineHeight = 26.sp, letterSpacing = if (isAndroidPlatform) 0.sp else MaterialTheme.typography.bodyLarge.letterSpacing, hyphens = if (isAndroidPlatform) Hyphens.Auto else Hyphens.Unspecified, @@ -808,7 +808,7 @@ private fun String.toReadAloudSpokenText(): String = private fun String.applyReadAloudTextReplacements(replacements: List): String = replacements.fold(this) { text, replacement -> - text.replace(replacement.from, replacement.to) + text.replace(replacement.from, replacement.to, ignoreCase = !replacement.caseSensitive) } private fun String.withReadAloudStressMarkers(): String = buildString(length) { @@ -886,9 +886,13 @@ private const val CombiningAcuteAccent = '\u0301' private const val ReadAloudStressableLetters = "аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouyAEIOUY" private val ReadAloudHardcodedTextReplacements = listOf( - ReadAloudTextReplacement("Господа,", "Господ/а,"), - ReadAloudTextReplacement("господа", "господ/а"), - ReadAloudTextReplacement("прошуршала", "прошурш/ала"), + ReadAloudTextReplacement(from = "Господа,", to = "Господ/а,", caseSensitive = true), + ReadAloudTextReplacement(from = "господа", to = "господ/а", caseSensitive = true), + ReadAloudTextReplacement(from = "прошуршала", to = "прошурш/ала", caseSensitive = false), + ReadAloudTextReplacement(from = "железной дорогой", to = "железной дор/огой"), + ReadAloudTextReplacement(from = "пустовавшими", to = "пустов/авшими"), + ReadAloudTextReplacement(from = "непарадной", to = "непар/адной"), + ReadAloudTextReplacement(from = "свертывали", to = "свёртывали"), ) private fun List.flattenSections(depth: Int = 0): List = diff --git a/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt b/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt new file mode 100644 index 0000000..92b8698 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt @@ -0,0 +1,72 @@ +package net.sergeych.toread + +import net.sergeych.toread.storage.BookReadingStatus +import kotlin.test.Test +import kotlin.test.assertEquals + +class LibraryScreenItemListTest { + @Test + fun appendNewLibraryItemsSkipsItemsAlreadyInMemory() { + val existing = listOf( + libraryItem("file-1", "First"), + libraryItem("file-2", "Second"), + ) + val page = listOf( + libraryItem("file-2", "Second duplicate"), + libraryItem("file-3", "Third"), + ) + + val merged = existing.appendNewLibraryItems(page) + + assertEquals(listOf("file-1", "file-2", "file-3"), merged.map { it.fileId }) + assertEquals("Second", merged[1].title) + } + + @Test + fun appendNewLibraryItemsDeduplicatesFirstPage() { + val page = listOf( + libraryItem("file-1", "First"), + libraryItem("file-1", "First duplicate"), + libraryItem("file-2", "Second"), + ) + + val merged = emptyList().appendNewLibraryItems(page) + + assertEquals(listOf("file-1", "file-2"), merged.map { it.fileId }) + assertEquals("First", merged.first().title) + } + + @Test + fun myLibraryShowsAllBooksExceptNotInterested() { + val items = listOf( + libraryItem("file-reading", "Reading", BookReadingStatus.READING), + libraryItem("file-to-read", "To Read", BookReadingStatus.TO_READ), + libraryItem("file-read", "Read", BookReadingStatus.READ), + libraryItem("file-new", "New", BookReadingStatus.NEW), + libraryItem("file-not-interested", "Not Interested", BookReadingStatus.NOT_INTERESTED), + ) + + val filtered = items.myLibraryItems() + + assertEquals( + listOf("file-new", "file-read", "file-reading", "file-to-read"), + filtered.map { it.fileId }, + ) + } + + private fun libraryItem( + fileId: String, + title: String, + readingStatus: BookReadingStatus = BookReadingStatus.NEW, + ): LibraryItem = + LibraryItem( + fileId = fileId, + bookId = "book-$fileId", + title = title, + format = "fb2", + sizeBytes = null, + storageUri = null, + lastSeenAt = null, + readingStatus = readingStatus, + ) +} diff --git a/composeApp/src/commonTest/kotlin/net/sergeych/toread/ReadAloudContentPlanTest.kt b/composeApp/src/commonTest/kotlin/net/sergeych/toread/ReadAloudContentPlanTest.kt index 54793e0..80c5b6c 100644 --- a/composeApp/src/commonTest/kotlin/net/sergeych/toread/ReadAloudContentPlanTest.kt +++ b/composeApp/src/commonTest/kotlin/net/sergeych/toread/ReadAloudContentPlanTest.kt @@ -89,6 +89,20 @@ class ReadAloudContentPlanTest { assertEquals("Господа́, идите.", plan.sentences.single().spokenText) } + @Test + fun hardcodedReadAloudReplacementsAreCaseSensitive() { + val plan = buildReaderContentPlan( + Fb2Book( + title = "Book", + sections = listOf( + Fb2Section(blocks = listOf(paragraph("ГОСПОДА, идите."))), + ), + ), + ) + + assertEquals("ГОСПОДА, идите.", plan.sentences.single().spokenText) + } + @Test fun stressMarkersSupportRussianAndEnglishVowels() { val plan = buildReaderContentPlan(