refactored library processing/views, better not interesting flag processing

This commit is contained in:
Sergey Chernov 2026-05-24 10:26:40 +03:00
parent de04b9cbed
commit c7b7ca68ab
4 changed files with 45 additions and 102 deletions

View File

@ -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<List<LibraryItem>>(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 loadedItems = loadLibraryItems()
if (loadedItems != previousItems) {
items = loadedItems
}
val visibleFileIds = page.mapTo(mutableSetOf()) { it.fileId }
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)
}
} else {
items = items.appendNewLibraryItems(page)
}
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<LibraryItem>.replaceLibraryItem(item: LibraryItem): List<LibraryItem> =
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 }
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

View File

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

View File

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

View File

@ -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<LibraryItem>().appendNewLibraryItems(page)
assertEquals(listOf("file-1", "file-2"), merged.map { it.fileId })
assertEquals("First", merged.first().title)
}
@Test
fun myLibraryShowsAllBooksExceptNotInterested() {
val items = listOf(