added library database
This commit is contained in:
1 parent
8f8b466e89
commit
02a40c2589
10 files changed
+383
-100
No files matched your search
@@ -58,6 +58,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -239,16 +240,42 @@ private fun LibraryScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
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) }
|
||||
val coverCache = remember { mutableStateMapOf<String, LibraryCover?>() }
|
||||
|
||||
suspend fun loadPage(reset: Boolean = false) {
|
||||
if (loadingPage) return
|
||||
loadingPage = true
|
||||
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
|
||||
nextOffset = offset + page.size
|
||||
endReached = page.size < LibraryPageSize
|
||||
} catch (t: Throwable) {
|
||||
message = t.message ?: "Could not load library."
|
||||
endReached = true
|
||||
} finally {
|
||||
loadingPage = false
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh(nextMessage: String? = message) {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
onStateChange(loadLibraryState(nextMessage, state.scanPath))
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
message = nextMessage
|
||||
scope.launch { loadPage(reset = true) }
|
||||
}
|
||||
|
||||
LaunchedEffect(state.scanPath, state.message) {
|
||||
if (items.isEmpty() && !endReached) loadPage(reset = true)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -259,7 +286,7 @@ private fun LibraryScreen(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = { refresh() }, enabled = !busy) {
|
||||
IconButton(onClick = { refresh() }, enabled = !busy && !loadingPage) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Refresh library")
|
||||
}
|
||||
},
|
||||
@@ -278,7 +305,11 @@ private fun LibraryScreen(
|
||||
.background(readerBackground()),
|
||||
) {
|
||||
val wide = maxWidth >= 800.dp
|
||||
if (state.items.isEmpty()) {
|
||||
if (items.isEmpty() && loadingPage) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (items.isEmpty()) {
|
||||
EmptyLibraryPane(modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp))
|
||||
} else {
|
||||
LazyColumn(
|
||||
@@ -286,9 +317,10 @@ private fun LibraryScreen(
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(state.items, key = { it.fileId }) { item ->
|
||||
items(items, key = { it.fileId }) { item ->
|
||||
LibraryRow(
|
||||
item = item,
|
||||
coverCache = coverCache,
|
||||
enabled = !busy,
|
||||
onOpen = {
|
||||
scope.launch {
|
||||
@@ -301,12 +333,12 @@ private fun LibraryScreen(
|
||||
AppState.Reader(
|
||||
fileId = item.fileId,
|
||||
book = book,
|
||||
libraryItems = state.items,
|
||||
libraryItems = items,
|
||||
scanPath = state.scanPath,
|
||||
message = message,
|
||||
)
|
||||
}.getOrElse {
|
||||
AppState.Library(state.items, state.scanPath, it.message ?: "Could not open book.")
|
||||
AppState.Library(items, state.scanPath, it.message ?: "Could not open book.")
|
||||
}
|
||||
onStateChange(next)
|
||||
} finally {
|
||||
@@ -319,12 +351,12 @@ private fun LibraryScreen(
|
||||
busy = true
|
||||
try {
|
||||
val deleted = runCatching { deleteLibraryItem(item.fileId) }.getOrDefault(false)
|
||||
onStateChange(
|
||||
loadLibraryState(
|
||||
if (deleted) "Removed ${item.title}." else "Could not remove ${item.title}.",
|
||||
state.scanPath,
|
||||
),
|
||||
)
|
||||
message = if (deleted) "Removed ${item.title}." else "Could not remove ${item.title}."
|
||||
if (deleted) {
|
||||
items = items.filterNot { it.fileId == item.fileId }
|
||||
coverCache.remove(item.fileId)
|
||||
nextOffset = (nextOffset - 1).coerceAtLeast(items.size)
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@@ -332,6 +364,19 @@ private fun LibraryScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!endReached) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,6 +528,7 @@ private fun EmptyLibraryPane(modifier: Modifier = Modifier) {
|
||||
@Composable
|
||||
private fun LibraryRow(
|
||||
item: LibraryItem,
|
||||
coverCache: MutableMap<String, LibraryCover?>,
|
||||
enabled: Boolean,
|
||||
onOpen: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
@@ -496,7 +542,7 @@ private fun LibraryRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
LibraryCover(item, modifier = Modifier.width(46.dp).aspectRatio(0.68f))
|
||||
LibraryCover(item, coverCache, modifier = Modifier.width(46.dp).aspectRatio(0.68f))
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
item.title,
|
||||
@@ -525,9 +571,19 @@ private fun LibraryRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryCover(item: LibraryItem, modifier: Modifier = Modifier.width(54.dp).aspectRatio(0.68f)) {
|
||||
val bitmap = remember(item.fileId, item.coverImage) {
|
||||
item.coverImage?.let(::decodeImageBytes)
|
||||
private fun LibraryCover(
|
||||
item: LibraryItem,
|
||||
coverCache: MutableMap<String, LibraryCover?>,
|
||||
modifier: Modifier = Modifier.width(54.dp).aspectRatio(0.68f),
|
||||
) {
|
||||
LaunchedEffect(item.fileId) {
|
||||
if (!coverCache.containsKey(item.fileId)) {
|
||||
coverCache[item.fileId] = loadLibraryItemCover(item.fileId)
|
||||
}
|
||||
}
|
||||
val cover = coverCache[item.fileId]
|
||||
val bitmap = remember(item.fileId, cover?.image) {
|
||||
cover?.image?.let(::decodeImageBytes)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -1283,7 +1339,7 @@ private suspend fun loadStartupState(): AppState {
|
||||
}
|
||||
}
|
||||
val activeFileId = loadActiveReadingFileId() ?: return library
|
||||
val item = library.items.firstOrNull { it.fileId == activeFileId }
|
||||
val item = loadLibraryItem(activeFileId)
|
||||
if (item == null) {
|
||||
saveActiveReadingFileId(null)
|
||||
return library
|
||||
@@ -1306,7 +1362,7 @@ private suspend fun loadStartupState(): AppState {
|
||||
private suspend fun loadLibraryState(message: String? = null, scanPath: String? = null): AppState =
|
||||
runCatching {
|
||||
AppState.Library(
|
||||
items = loadLibraryItems(),
|
||||
items = emptyList(),
|
||||
scanPath = scanPath ?: defaultLibraryScanPath().orEmpty(),
|
||||
message = message,
|
||||
)
|
||||
@@ -1387,6 +1443,8 @@ fun Fb2Binary.imageBytes(): ByteArray = Base64.Default.decode(base64)
|
||||
|
||||
const val DefaultBookFileName: String = "Maraini_Zapiski-Terezy-Numy.G7vc8A.872381.fb2.zip"
|
||||
|
||||
private const val LibraryPageSize: Int = 50
|
||||
|
||||
expect fun loadDefaultBookBytes(): ByteArray?
|
||||
|
||||
expect fun decodeBookImage(binary: Fb2Binary): ImageBitmap?
|
||||
|
||||
@@ -18,6 +18,11 @@ data class LibraryItem(
|
||||
val coverImageMimeType: String? = null,
|
||||
)
|
||||
|
||||
data class LibraryCover(
|
||||
val image: ByteArray,
|
||||
val mimeType: String?,
|
||||
)
|
||||
|
||||
data class LibraryScanReport(
|
||||
val scannedFiles: Int,
|
||||
val importedFiles: Int,
|
||||
@@ -77,6 +82,12 @@ expect suspend fun chooseLibraryScanDirectory(): String?
|
||||
|
||||
expect suspend fun loadLibraryItems(): List<LibraryItem>
|
||||
|
||||
expect suspend fun loadLibraryItemsPage(limit: Int, offset: Int): List<LibraryItem>
|
||||
|
||||
expect suspend fun loadLibraryItem(fileId: String): LibraryItem?
|
||||
|
||||
expect suspend fun loadLibraryItemCover(fileId: String): LibraryCover?
|
||||
|
||||
expect suspend fun scanLibrarySubtree(path: String, onProgress: (LibraryScanProgress) -> Unit): LibraryScanReport
|
||||
|
||||
expect suspend fun openLibraryBook(fileId: String): ByteArray?
|
||||
|
||||
Reference in new issue
Block a user