diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt index 0d985b0..0b0bd53 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt @@ -97,9 +97,7 @@ internal fun LibraryScreen( var busy by remember { mutableStateOf(false) } var message by remember(state.message) { mutableStateOf(state.message) } var items by remember(state.items) { mutableStateOf(state.items) } - var nextOffset by remember(state.items) { mutableStateOf(state.items.size) } - var loadingPage by remember(state.items) { mutableStateOf(false) } - var endReached by remember(state.items) { mutableStateOf(false) } + var loadingLibrary by remember(state.items) { mutableStateOf(false) } var recentlyAddedItems by remember(state.items) { mutableStateOf>(emptyList()) } var wasScanning by remember { mutableStateOf(false) } var settingsMenuOpen by remember { mutableStateOf(false) } @@ -121,38 +119,26 @@ internal fun LibraryScreen( 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) { - if (loadingPage) return - loadingPage = true + suspend fun loadLibrary() { + if (loadingLibrary) return + loadingLibrary = true val previousItems = items - if (reset) { - nextOffset = 0 - endReached = false - } - val offset = if (reset) 0 else nextOffset try { - val limit = if (reset) maxOf(LibraryPageSize, previousItems.size) else LibraryPageSize - val page = loadLibraryItemsPage(limit, offset) - if (reset) { - if (page != previousItems) { - items = page - } - val visibleFileIds = page.mapTo(mutableSetOf()) { it.fileId } - coverCache.keys.toList().forEach { fileId -> - if (fileId !in visibleFileIds) coverCache.remove(fileId) - } - } else { - items = items.appendNewLibraryItems(page) + val loadedItems = loadLibraryItems() + if (loadedItems != previousItems) { + items = loadedItems + } + val visibleFileIds = loadedItems.mapTo(mutableSetOf()) { it.fileId } + recentlyAddedItems.mapTo(visibleFileIds) { it.fileId } + searchResults.mapTo(visibleFileIds) { it.fileId } + coverCache.keys.toList().forEach { fileId -> + if (fileId !in visibleFileIds) coverCache.remove(fileId) } - nextOffset = offset + page.size - endReached = page.size < limit } catch (t: Throwable) { message = t.message ?: strings.couldNotLoadLibrary - endReached = true } finally { - loadingPage = false + loadingLibrary = false } } @@ -169,7 +155,7 @@ internal fun LibraryScreen( searchResults = searchLibraryItems(searchText, SearchResultLimit) searching = false } else { - loadPage(reset = true) + loadLibrary() loadRecentlyAdded() } } @@ -191,7 +177,7 @@ internal fun LibraryScreen( searchResults = searchLibraryItems(searchText, SearchResultLimit) searching = false } else { - loadPage(reset = true) + loadLibrary() loadRecentlyAdded() } } else { @@ -215,7 +201,7 @@ internal fun LibraryScreen( searchResults = searchLibraryItems(searchText, SearchResultLimit) searching = false } else { - loadPage(reset = true) + loadLibrary() loadRecentlyAdded() } } else { @@ -255,8 +241,8 @@ internal fun LibraryScreen( } LaunchedEffect(state.scanPath, state.message) { - if (items.isEmpty() && !endReached) { - loadPage(reset = true) + if (items.isEmpty()) { + loadLibrary() loadRecentlyAdded() } } @@ -293,9 +279,9 @@ internal fun LibraryScreen( } } - LaunchedEffect(searchActive, loadingPage, endReached, libraryItems, recentlyAdded) { - val libraryDataLoaded = endReached || libraryItems.isNotEmpty() || recentlyAdded.isNotEmpty() - if (!filterChosenByUser && !searchActive && !loadingPage && libraryDataLoaded) { + LaunchedEffect(searchActive, loadingLibrary, libraryItems, recentlyAdded) { + val libraryDataLoaded = libraryItems.isNotEmpty() || recentlyAdded.isNotEmpty() + if (!filterChosenByUser && !searchActive && !loadingLibrary && libraryDataLoaded) { selectedFilter = defaultLibraryFilter(libraryItems, recentlyAdded) } } @@ -328,12 +314,12 @@ internal fun LibraryScreen( wasScanning = true while (true) { delay(2_000) - loadPage(reset = true) + loadLibrary() loadRecentlyAdded() } } else if (wasScanning) { wasScanning = false - loadPage(reset = true) + loadLibrary() loadRecentlyAdded() } } @@ -486,11 +472,11 @@ internal fun LibraryScreen( .background(readerBackground()), ) { val wide = maxWidth >= 800.dp - if (visibleItems.isEmpty() && (loadingPage || searching)) { + if (visibleItems.isEmpty() && (loadingLibrary || searching)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } - } else if (visibleItems.isEmpty() && !canLoadMore) { + } else if (visibleItems.isEmpty()) { if (searchActive) { EmptySearchPane(modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp)) } else { @@ -520,7 +506,7 @@ internal fun LibraryScreen( readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem) coverCache[updatedItem.fileId] = loadLibraryItemCover(updatedItem.fileId) } - if (item.readingStatus == BookReadingStatus.NEW && + if (item.readingStatus.shouldBecomeReadingOnOpen() && markLibraryReadingStatus(item.fileId, BookReadingStatus.READING) ) { val readingItem = loadLibraryItem(item.fileId) @@ -613,7 +599,6 @@ internal fun LibraryScreen( val previousRecentlyAddedItems = recentlyAddedItems val previousCover = coverCache[item.fileId] val hadCover = coverCache.containsKey(item.fileId) - val previousNextOffset = nextOffset onDeleteRequested( LibraryDeleteRequest(item.fileId, item.title), { @@ -622,7 +607,6 @@ internal fun LibraryScreen( searchResults = searchResults.filterNot { it.fileId == item.fileId } recentlyAddedItems = recentlyAddedItems.filterNot { it.fileId == item.fileId } coverCache.remove(item.fileId) - nextOffset = (nextOffset - 1).coerceAtLeast(items.size) }, { items = previousItems @@ -631,7 +615,6 @@ internal fun LibraryScreen( if (hadCover) { coverCache[item.fileId] = previousCover } - nextOffset = previousNextOffset }, ) }, @@ -649,20 +632,6 @@ internal fun LibraryScreen( } libraryRows(visibleItems) - - if (canLoadMore) { - item(key = "load-more") { - LaunchedEffect(nextOffset, items.size) { - if (!loadingPage) loadPage() - } - Box( - modifier = Modifier.fillMaxWidth().padding(18.dp), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(modifier = Modifier.width(24.dp).height(24.dp), strokeWidth = 2.dp) - } - } - } } } activeScan?.let { progress -> @@ -1002,9 +971,9 @@ private data class LibraryItemActions( val onDelete: () -> Unit, ) -private enum class LibraryFilter(val usesPagedLibrary: Boolean = true) { +private enum class LibraryFilter { ReadingNow, - RecentlyAdded(usesPagedLibrary = false), + RecentlyAdded, MyLibrary, ToRead, Favorites, @@ -1193,15 +1162,12 @@ 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 } +internal fun BookReadingStatus.shouldBecomeReadingOnOpen(): Boolean = + this == BookReadingStatus.NEW || this == BookReadingStatus.NOT_INTERESTED + private fun Key.isEnterKey(): Boolean = this == Key.Enter || this == Key.NumPadEnter private fun LibraryScanProgress.toCatalogScanMessage(): String { @@ -1214,7 +1180,6 @@ private fun LibraryScanProgress.toCatalogScanMessage(): String { return strings.scannedProgress(scannedFiles, total, percent) } -private const val LibraryPageSize: Int = 50 private const val SearchResultLimit: Int = 100 private const val RecentlyAddedLimit: Int = 50 private const val RecentlyAddedWindowMillis: Long = 30L * 60L * 60L * 1000L diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/Localization.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/Localization.kt index 296a597..2321975 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/Localization.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/Localization.kt @@ -267,7 +267,7 @@ internal object RussianStrings : AppStrings() { override val closeSearch = "Закрыть поиск" override val clearSearch = "Очистить поиск" override val noMatches = "Ничего не найдено" - override val scanFolderOrChooseFilter = "Просканируйте папку или выберите другой фильтр библиотеки." + override val scanFolderOrChooseFilter = "Импортируйте папку или выберите другой фильтр библиотеки." override val favorite = "Избранное" override val unknownAuthor = "Автор неизвестен" override val noMetadata = "Нет метаданных" @@ -384,9 +384,9 @@ internal object RussianStrings : AppStrings() { } override fun importedSkippedFailed(imported: Int, skipped: Int, failed: Int): String = "Импортировано: $imported, пропущено: $skipped, ошибок: $failed" - override fun scannedBooks(scanned: Int): String = "Просканировано книг: $scanned" + override fun scannedBooks(scanned: Int): String = "Импортировано книг: $scanned" override fun scannedProgress(scanned: Int, total: Int, percent: Int): String = - "Просканировано $scanned из $total, готово $percent%" + "Импорт: $scanned из $total, готово $percent%" override fun noBooksIn(filterLabel: String): String = "Нет книг: ${filterLabel.lowercase()}" override fun bookMenuFor(title: String): String = "Меню книги: $title" override fun markedAsRead(title: String?): String = diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt index 8591de4..a13deb3 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt @@ -110,6 +110,15 @@ internal fun BookView( libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = status) if (status == BookReadingStatus.READ) markedRead = true if (status == BookReadingStatus.NEW) markedRead = false + if (status == BookReadingStatus.NOT_INTERESTED) { + saveLibraryReadingPosition( + fileId, + ReadingPosition(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset), + ) + saveActiveReadingFileId(null) + onBack() + return@launch + } showMessage(successMessage) } else { showMessage(strings.couldNotUpdateBook) @@ -121,7 +130,7 @@ internal fun BookView( val item = loadLibraryItem(fileId) libraryItem = item markedRead = item?.readingStatus == BookReadingStatus.READ - if (item?.readingStatus == BookReadingStatus.NEW && markLibraryReadingStatus(fileId, BookReadingStatus.READING)) { + if (item?.readingStatus?.shouldBecomeReadingOnOpen() == true && markLibraryReadingStatus(fileId, BookReadingStatus.READING)) { libraryItem = loadLibraryItem(fileId) ?: item.copy(readingStatus = BookReadingStatus.READING) } } diff --git a/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt b/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt index 92b8698..91b2834 100644 --- a/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt +++ b/composeApp/src/commonTest/kotlin/net/sergeych/toread/LibraryScreenItemListTest.kt @@ -5,37 +5,6 @@ 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(