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