refactored library processing/views, better not interesting flag processing
This commit is contained in:
parent
de04b9cbed
commit
c7b7ca68ab
@ -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 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<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
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user