fixed bug with returning from the reading state and search

This commit is contained in:
Sergey Chernov 2026-05-24 08:01:47 +03:00
parent b0f45aaf1b
commit 1c6a80c43b
10 changed files with 113 additions and 50 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata !*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings **/xcshareddata/WorkspaceSettings.xcsettings
node_modules/ node_modules/
/composeApp/release/

View File

@ -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
}

View File

@ -120,6 +120,7 @@ internal fun LibraryScreen(
val sourceItems = if (searchActive) visibleSearchResults else libraryItems val sourceItems = if (searchActive) visibleSearchResults else libraryItems
val recentlyAdded = recentlyAddedItems.filterNot { it.fileId in hiddenFileIds } val recentlyAdded = recentlyAddedItems.filterNot { it.fileId in hiddenFileIds }
val visibleItems = selectedFilter.apply(sourceItems, recentlyAdded, searchActive) val visibleItems = selectedFilter.apply(sourceItems, recentlyAdded, searchActive)
.withoutDuplicateFileIds()
val canLoadMore = !searchActive && selectedFilter.usesPagedLibrary && !endReached val canLoadMore = !searchActive && selectedFilter.usesPagedLibrary && !endReached
suspend fun loadPage(reset: Boolean = false) { suspend fun loadPage(reset: Boolean = false) {
@ -143,7 +144,7 @@ internal fun LibraryScreen(
if (fileId !in visibleFileIds) coverCache.remove(fileId) if (fileId !in visibleFileIds) coverCache.remove(fileId)
} }
} else { } else {
items = items + page items = items.appendNewLibraryItems(page)
} }
nextOffset = offset + page.size nextOffset = offset + page.size
endReached = page.size < limit endReached = page.size < limit
@ -1032,13 +1033,7 @@ private enum class LibraryFilter(val usesPagedLibrary: Boolean = true) {
.filter { it.readingStatus == BookReadingStatus.NEW } .filter { it.readingStatus == BookReadingStatus.NEW }
.sortedByImportedThenTitle() .sortedByImportedThenTitle()
} }
MyLibrary -> sourceItems.filter { MyLibrary -> sourceItems.myLibraryItems()
it.fileId !in recentlyAddedIds &&
it.readingStatus != BookReadingStatus.READING &&
it.readingStatus != BookReadingStatus.TO_READ &&
it.readingStatus != BookReadingStatus.READ &&
it.readingStatus != BookReadingStatus.NOT_INTERESTED
}.sortedByTitleNaturally()
ToRead -> sourceItems ToRead -> sourceItems
.filter { it.readingStatus == BookReadingStatus.TO_READ } .filter { it.readingStatus == BookReadingStatus.TO_READ }
.sortedByLastReadThenTitle() .sortedByLastReadThenTitle()
@ -1070,6 +1065,10 @@ private fun List<LibraryItem>.sortedByImportedThenTitle(): List<LibraryItem> =
private fun List<LibraryItem>.sortedByTitleNaturally(): List<LibraryItem> = private fun List<LibraryItem>.sortedByTitleNaturally(): List<LibraryItem> =
sortedWith(LibraryItemNaturalTitleComparator) sortedWith(LibraryItemNaturalTitleComparator)
internal fun List<LibraryItem>.myLibraryItems(): List<LibraryItem> =
filter { it.readingStatus != BookReadingStatus.NOT_INTERESTED }
.sortedByTitleNaturally()
private val LibraryItemNaturalTitleComparator = Comparator<LibraryItem> { left, right -> private val LibraryItemNaturalTitleComparator = Comparator<LibraryItem> { left, right ->
val titleCompare = naturalCompare(left.title, right.title) val titleCompare = naturalCompare(left.title, right.title)
if (titleCompare != 0) titleCompare else left.fileId.compareTo(right.fileId) if (titleCompare != 0) titleCompare else left.fileId.compareTo(right.fileId)
@ -1194,6 +1193,15 @@ private fun Long.formatBytes(): String =
private fun List<LibraryItem>.replaceLibraryItem(item: LibraryItem): List<LibraryItem> = private fun List<LibraryItem>.replaceLibraryItem(item: LibraryItem): List<LibraryItem> =
map { current -> if (current.fileId == item.fileId) item else current } map { current -> if (current.fileId == item.fileId) item else current }
internal fun List<LibraryItem>.appendNewLibraryItems(page: List<LibraryItem>): List<LibraryItem> {
if (isEmpty()) return page.withoutDuplicateFileIds()
val fileIds = mapTo(mutableSetOf()) { it.fileId }
return this + page.filter { fileIds.add(it.fileId) }
}
internal fun List<LibraryItem>.withoutDuplicateFileIds(): List<LibraryItem> =
distinctBy { it.fileId }
private fun Key.isEnterKey(): Boolean = this == Key.Enter || this == Key.NumPadEnter private fun Key.isEnterKey(): Boolean = this == Key.Enter || this == Key.NumPadEnter
private fun LibraryScanProgress.toCatalogScanMessage(): String { private fun LibraryScanProgress.toCatalogScanMessage(): String {

View File

@ -34,6 +34,7 @@ data class ReadAloudVoiceOption(
data class ReadAloudTextReplacement( data class ReadAloudTextReplacement(
val from: String, val from: String,
val to: String, val to: String,
val caseSensitive: Boolean = false,
) )
data class ReadAloudSettingsState( data class ReadAloudSettingsState(

View File

@ -537,7 +537,7 @@ private fun ReaderText(
private fun readerParagraphTextStyle(language: String?): TextStyle = private fun readerParagraphTextStyle(language: String?): TextStyle =
MaterialTheme.typography.bodyLarge.copy( MaterialTheme.typography.bodyLarge.copy(
fontWeight = if( isAndroidPlatform) FontWeight(350) else FontWeight.Normal, 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, lineHeight = 26.sp,
letterSpacing = if (isAndroidPlatform) 0.sp else MaterialTheme.typography.bodyLarge.letterSpacing, letterSpacing = if (isAndroidPlatform) 0.sp else MaterialTheme.typography.bodyLarge.letterSpacing,
hyphens = if (isAndroidPlatform) Hyphens.Auto else Hyphens.Unspecified, hyphens = if (isAndroidPlatform) Hyphens.Auto else Hyphens.Unspecified,
@ -808,7 +808,7 @@ private fun String.toReadAloudSpokenText(): String =
private fun String.applyReadAloudTextReplacements(replacements: List<ReadAloudTextReplacement>): String = private fun String.applyReadAloudTextReplacements(replacements: List<ReadAloudTextReplacement>): String =
replacements.fold(this) { text, replacement -> 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) { private fun String.withReadAloudStressMarkers(): String = buildString(length) {
@ -886,9 +886,13 @@ private const val CombiningAcuteAccent = '\u0301'
private const val ReadAloudStressableLetters = "аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouyAEIOUY" private const val ReadAloudStressableLetters = "аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouyAEIOUY"
private val ReadAloudHardcodedTextReplacements = listOf( private val ReadAloudHardcodedTextReplacements = listOf(
ReadAloudTextReplacement("Господа,", "Господ/а,"), ReadAloudTextReplacement(from = "Господа,", to = "Господ/а,", caseSensitive = true),
ReadAloudTextReplacement("господа", "господ/а"), ReadAloudTextReplacement(from = "господа", to = "господ/а", caseSensitive = true),
ReadAloudTextReplacement("прошуршала", "прошурш/ала"), ReadAloudTextReplacement(from = "прошуршала", to = "прошурш/ала", caseSensitive = false),
ReadAloudTextReplacement(from = "железной дорогой", to = "железной дор/огой"),
ReadAloudTextReplacement(from = "пустовавшими", to = "пустов/авшими"),
ReadAloudTextReplacement(from = "непарадной", to = "непар/адной"),
ReadAloudTextReplacement(from = "свертывали", to = "свёртывали"),
) )
private fun List<Fb2Section>.flattenSections(depth: Int = 0): List<ChapterEntry> = private fun List<Fb2Section>.flattenSections(depth: Int = 0): List<ChapterEntry> =

View File

@ -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<LibraryItem>().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,
)
}

View File

@ -89,6 +89,20 @@ class ReadAloudContentPlanTest {
assertEquals("Господа́, идите.", plan.sentences.single().spokenText) 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 @Test
fun stressMarkersSupportRussianAndEnglishVowels() { fun stressMarkersSupportRussianAndEnglishVowels() {
val plan = buildReaderContentPlan( val plan = buildReaderContentPlan(