View/logic improvements
This commit is contained in:
1 parent
da88a6e221
commit
26343cb8a1
10 files changed
+386
-25
No files matched your search
@@ -103,6 +103,41 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
|
||||
state = loadStartupState()
|
||||
}
|
||||
|
||||
fun navigateBack() {
|
||||
imageViewer?.let {
|
||||
imageViewer = null
|
||||
return
|
||||
}
|
||||
state = when (val current = state) {
|
||||
is AppState.BookInfo -> AppState.Reader(
|
||||
fileId = current.fileId,
|
||||
book = current.book,
|
||||
libraryItems = current.libraryItems,
|
||||
scanPath = current.scanPath,
|
||||
message = current.message,
|
||||
)
|
||||
is AppState.Reader -> {
|
||||
scope.launch { saveActiveReadingFileId(null) }
|
||||
AppState.Library(current.libraryItems, current.scanPath, current.message)
|
||||
}
|
||||
is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message)
|
||||
is AppState.Error -> AppState.LoadingLibrary
|
||||
is AppState.Library, AppState.LoadingLibrary -> current
|
||||
}
|
||||
}
|
||||
|
||||
val navigationDepth = when (state) {
|
||||
is AppState.BookInfo -> 2
|
||||
is AppState.Error, is AppState.Reader, is AppState.Scan -> 1
|
||||
is AppState.Library, AppState.LoadingLibrary -> 0
|
||||
} + if (imageViewer != null) 1 else 0
|
||||
|
||||
PlatformBackHandler(
|
||||
enabled = navigationDepth > 0,
|
||||
navigationDepth = navigationDepth,
|
||||
onBack = ::navigateBack,
|
||||
)
|
||||
|
||||
fun startScan(path: String) {
|
||||
if (scanJob?.isActive == true) return
|
||||
activeScan = LibraryScanProgress(0, 0, 0, 0)
|
||||
@@ -126,7 +161,17 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
|
||||
scanJob = null
|
||||
activeScan = null
|
||||
state = when (val current = state) {
|
||||
is AppState.Library, is AppState.Scan, AppState.LoadingLibrary -> loadLibraryState(message, path)
|
||||
is AppState.Library -> if (report.getOrNull()?.hasLibraryChanges() == true) {
|
||||
loadLibraryState(message, path)
|
||||
} else {
|
||||
current.copy(scanPath = path, message = message)
|
||||
}
|
||||
is AppState.Scan -> if (report.getOrNull()?.hasLibraryChanges() == true) {
|
||||
loadLibraryState(message, path)
|
||||
} else {
|
||||
AppState.Library(current.items, path, message)
|
||||
}
|
||||
AppState.LoadingLibrary -> loadLibraryState(message, path)
|
||||
is AppState.Reader -> current.copy(message = message)
|
||||
is AppState.BookInfo -> current.copy(message = message)
|
||||
is AppState.Error -> current
|
||||
@@ -169,31 +214,21 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
|
||||
onDeleted = { message ->
|
||||
state = AppState.Library(emptyList(), current.scanPath, message)
|
||||
},
|
||||
onBack = {
|
||||
state = AppState.Library(emptyList(), current.scanPath, current.message)
|
||||
},
|
||||
onBack = ::navigateBack,
|
||||
)
|
||||
is AppState.BookInfo -> BookInfoScreen(
|
||||
fileId = current.fileId,
|
||||
book = current.book,
|
||||
onImageOpen = { imageViewer = it },
|
||||
onBack = {
|
||||
state = AppState.Reader(
|
||||
fileId = current.fileId,
|
||||
book = current.book,
|
||||
libraryItems = current.libraryItems,
|
||||
scanPath = current.scanPath,
|
||||
message = current.message,
|
||||
)
|
||||
},
|
||||
onBack = ::navigateBack,
|
||||
)
|
||||
is AppState.Error -> ErrorScreen(current.message, onBack = { state = AppState.LoadingLibrary })
|
||||
is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack)
|
||||
}
|
||||
|
||||
imageViewer?.let { image ->
|
||||
ImageViewer(
|
||||
image = image,
|
||||
onBack = { imageViewer = null },
|
||||
onBack = ::navigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -204,3 +239,6 @@ internal data class ViewedBookImage(
|
||||
val mimeType: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
private fun LibraryScanReport.hasLibraryChanges(): Boolean =
|
||||
importedFiles > 0
|
||||
@@ -44,6 +44,12 @@ data class LibraryScanProgress(
|
||||
val totalFiles: Int? = null,
|
||||
)
|
||||
|
||||
data class LibraryRescanReport(
|
||||
val scannedFiles: Int,
|
||||
val updatedFiles: Int,
|
||||
val failedFiles: Int,
|
||||
)
|
||||
|
||||
data class PlatformOpenBookRequest(
|
||||
val id: String,
|
||||
val displayName: String,
|
||||
@@ -100,6 +106,10 @@ expect suspend fun scanLibrarySubtree(path: String, onProgress: (LibraryScanProg
|
||||
|
||||
expect suspend fun openLibraryBook(fileId: String): ByteArray?
|
||||
|
||||
expect suspend fun refreshLibraryItemFromParsedBook(fileId: String, book: Fb2Book): LibraryItem?
|
||||
|
||||
expect suspend fun rescanAllLibraryBooks(): LibraryRescanReport
|
||||
|
||||
expect suspend fun deleteLibraryItem(fileId: String): Boolean
|
||||
|
||||
expect suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition?
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
@@ -62,6 +61,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.sergeych.toread.fb2.Fb2Format
|
||||
@@ -103,18 +103,28 @@ internal fun LibraryScreen(
|
||||
suspend fun loadPage(reset: Boolean = false) {
|
||||
if (loadingPage) return
|
||||
loadingPage = true
|
||||
val previousItems = items
|
||||
if (reset) {
|
||||
items = emptyList()
|
||||
nextOffset = 0
|
||||
endReached = false
|
||||
coverCache.clear()
|
||||
}
|
||||
val offset = if (reset) 0 else nextOffset
|
||||
try {
|
||||
val page = loadLibraryItemsPage(LibraryPageSize, offset)
|
||||
items = if (reset) page else items + page
|
||||
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 + page
|
||||
}
|
||||
nextOffset = offset + page.size
|
||||
endReached = page.size < LibraryPageSize
|
||||
endReached = page.size < limit
|
||||
} catch (t: Throwable) {
|
||||
message = t.message ?: "Could not load library."
|
||||
endReached = true
|
||||
@@ -136,6 +146,24 @@ internal fun LibraryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun rescanAllLibrary() {
|
||||
settingsMenuOpen = false
|
||||
scope.launch {
|
||||
busy = true
|
||||
message = "Rescanning library..."
|
||||
try {
|
||||
val report = rescanAllLibraryBooks()
|
||||
refresh(
|
||||
"Rescanned ${report.scannedFiles}, updated ${report.updatedFiles}, failed ${report.failedFiles}.",
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
message = t.message ?: "Library rescan failed."
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearch() {
|
||||
searchText = ""
|
||||
searchResults = emptyList()
|
||||
@@ -212,14 +240,17 @@ internal fun LibraryScreen(
|
||||
},
|
||||
colors = themedTopAppBarColors(),
|
||||
actions = {
|
||||
IconButton(onClick = { refresh() }, enabled = !busy && !loadingPage) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Refresh library")
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { settingsMenuOpen = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = "Library options")
|
||||
}
|
||||
DropdownMenu(expanded = settingsMenuOpen, onDismissRequest = { settingsMenuOpen = false }) {
|
||||
// DropdownMenuItem(
|
||||
// text = { Text("Rescan all library") },
|
||||
// enabled = !busy && activeScan == null,
|
||||
// onClick = ::rescanAllLibrary,
|
||||
// )
|
||||
// HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Checkbox(checked = autoScanDownloads, onCheckedChange = null)
|
||||
@@ -282,12 +313,19 @@ internal fun LibraryScreen(
|
||||
val next = runCatching {
|
||||
val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.")
|
||||
val book = Fb2Format.parse(bytes, item.storageUri ?: item.title)
|
||||
var readerLibraryItems = visibleItems
|
||||
refreshLibraryItemFromParsedBook(item.fileId, book)?.let { updatedItem ->
|
||||
items = items.replaceLibraryItem(updatedItem)
|
||||
searchResults = searchResults.replaceLibraryItem(updatedItem)
|
||||
readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem)
|
||||
coverCache[updatedItem.fileId] = loadLibraryItemCover(updatedItem.fileId)
|
||||
}
|
||||
markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)
|
||||
saveActiveReadingFileId(item.fileId)
|
||||
AppState.Reader(
|
||||
fileId = item.fileId,
|
||||
book = book,
|
||||
libraryItems = visibleItems,
|
||||
libraryItems = readerLibraryItems,
|
||||
scanPath = state.scanPath,
|
||||
message = message,
|
||||
)
|
||||
@@ -639,12 +677,14 @@ private fun LibraryRow(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
item.authors.joinToString().ifBlank { "Unknown author" },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
item.libraryMetadataLine(),
|
||||
@@ -791,6 +831,9 @@ private fun Long.formatBytes(): String =
|
||||
else -> "$this B"
|
||||
}
|
||||
|
||||
private fun List<LibraryItem>.replaceLibraryItem(item: LibraryItem): List<LibraryItem> =
|
||||
map { current -> if (current.fileId == item.fileId) item else current }
|
||||
|
||||
private fun LibraryScanProgress.toCatalogScanMessage(): String {
|
||||
val total = totalFiles ?: return "Scanned $scannedFiles books"
|
||||
val percent = if (total <= 0) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.sergeych.toread
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
internal expect fun PlatformBackHandler(
|
||||
enabled: Boolean,
|
||||
navigationDepth: Int,
|
||||
onBack: () -> Unit,
|
||||
)
|
||||
Reference in new issue
Block a user