fixed bug with returning from the reading state and search
This commit is contained in:
parent
b0f45aaf1b
commit
1c6a80c43b
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ captures
|
|||||||
!*.xcworkspace/contents.xcworkspacedata
|
!*.xcworkspace/contents.xcworkspacedata
|
||||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||||
node_modules/
|
node_modules/
|
||||||
|
/composeApp/release/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
|
||||||
}
|
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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> =
|
||||||
|
|||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user