better library organization
This commit is contained in:
1 parent
06b4614cbe
commit
ddf68c1e6a
3 files changed
+236
-126
No files matched your search
@@ -17,12 +17,15 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
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
|
||||
@@ -90,6 +93,9 @@ internal fun LibraryScreen(
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
var searchResults by remember { mutableStateOf<List<LibraryItem>>(emptyList()) }
|
||||
var searching by remember { mutableStateOf(false) }
|
||||
var readingNowCollapsed by remember { mutableStateOf(false) }
|
||||
var myLibraryCollapsed by remember { mutableStateOf(false) }
|
||||
var notInterestedCollapsed by remember { mutableStateOf(false) }
|
||||
val coverCache = remember { mutableStateMapOf<String, LibraryCover?>() }
|
||||
val searchActive = searchText.isNotBlank()
|
||||
val visibleItems = if (searchActive) searchResults else items
|
||||
@@ -268,112 +274,156 @@ internal fun LibraryScreen(
|
||||
contentPadding = PaddingValues(bottom = if (activeScan != null) 88.dp else 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
val hasReadingNow = !searchActive && visibleItems.firstOrNull()?.readingStatus == BookReadingStatus.READING
|
||||
if (hasReadingNow) {
|
||||
item(key = "section-reading") {
|
||||
LibrarySectionHeader("reading now")
|
||||
fun itemActions(item: LibraryItem) = LibraryItemActions(
|
||||
onOpen = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val next = runCatching {
|
||||
val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.")
|
||||
val book = Fb2Format.parse(bytes, item.storageUri ?: item.title)
|
||||
markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)
|
||||
saveActiveReadingFileId(item.fileId)
|
||||
AppState.Reader(
|
||||
fileId = item.fileId,
|
||||
book = book,
|
||||
libraryItems = visibleItems,
|
||||
scanPath = state.scanPath,
|
||||
message = message,
|
||||
)
|
||||
}.getOrElse {
|
||||
AppState.Library(visibleItems, state.scanPath, it.message ?: "Could not open book.")
|
||||
}
|
||||
onStateChange(next)
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkAsRead = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READ)) {
|
||||
message = "Marked ${item.title} as read."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkAsUnread = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) {
|
||||
message = "Marked ${item.title} as unread."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onRemoveMarks = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) {
|
||||
message = "Removed marks from ${item.title}."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onNotInterested = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NOT_INTERESTED)) {
|
||||
message = "Marked ${item.title} as not interested."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val deleted = runCatching { deleteLibraryItem(item.fileId) }.getOrDefault(false)
|
||||
message = if (deleted) "Removed ${item.title}." else "Could not remove ${item.title}."
|
||||
if (deleted) {
|
||||
items = items.filterNot { it.fileId == item.fileId }
|
||||
searchResults = searchResults.filterNot { it.fileId == item.fileId }
|
||||
coverCache.remove(item.fileId)
|
||||
nextOffset = (nextOffset - 1).coerceAtLeast(items.size)
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun LazyListScope.libraryRows(sectionKey: String, sectionItems: List<LibraryItem>) {
|
||||
itemsIndexed(sectionItems, key = { _, item -> "$sectionKey-${item.fileId}" }) { _, item ->
|
||||
LibraryRow(
|
||||
item = item,
|
||||
coverCache = coverCache,
|
||||
enabled = !busy,
|
||||
actions = itemActions(item),
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(visibleItems, key = { _, item -> item.fileId }) { index, item ->
|
||||
if (
|
||||
hasReadingNow &&
|
||||
item.readingStatus != BookReadingStatus.READING &&
|
||||
(index == 0 || visibleItems[index - 1].readingStatus == BookReadingStatus.READING)
|
||||
|
||||
if (searchActive) {
|
||||
libraryRows("search", visibleItems)
|
||||
} else {
|
||||
val readingNow = visibleItems.filter { it.readingStatus == BookReadingStatus.READING }
|
||||
val myLibrary = visibleItems.filter { it.readingStatus != BookReadingStatus.READING && it.readingStatus != BookReadingStatus.NOT_INTERESTED }
|
||||
val notInterested = visibleItems.filter { it.readingStatus == BookReadingStatus.NOT_INTERESTED }
|
||||
|
||||
librarySection(
|
||||
key = "reading",
|
||||
title = "reading now",
|
||||
count = readingNow.size,
|
||||
collapsed = readingNowCollapsed,
|
||||
onCollapsedChange = { readingNowCollapsed = it },
|
||||
) {
|
||||
LibrarySectionHeader("my library")
|
||||
libraryRows("reading", readingNow)
|
||||
}
|
||||
librarySection(
|
||||
key = "library",
|
||||
title = "my library",
|
||||
count = myLibrary.size,
|
||||
collapsed = myLibraryCollapsed,
|
||||
onCollapsedChange = { myLibraryCollapsed = it },
|
||||
) {
|
||||
libraryRows("library", myLibrary)
|
||||
}
|
||||
librarySection(
|
||||
key = "not-interested",
|
||||
title = "not interested",
|
||||
count = notInterested.size,
|
||||
collapsed = notInterestedCollapsed,
|
||||
onCollapsedChange = { notInterestedCollapsed = it },
|
||||
) {
|
||||
libraryRows("not-interested", notInterested)
|
||||
}
|
||||
LibraryRow(
|
||||
item = item,
|
||||
coverCache = coverCache,
|
||||
enabled = !busy,
|
||||
onOpen = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val next = runCatching {
|
||||
val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.")
|
||||
val book = Fb2Format.parse(bytes, item.storageUri ?: item.title)
|
||||
markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)
|
||||
saveActiveReadingFileId(item.fileId)
|
||||
AppState.Reader(
|
||||
fileId = item.fileId,
|
||||
book = book,
|
||||
libraryItems = visibleItems,
|
||||
scanPath = state.scanPath,
|
||||
message = message,
|
||||
)
|
||||
}.getOrElse {
|
||||
AppState.Library(visibleItems, state.scanPath, it.message ?: "Could not open book.")
|
||||
}
|
||||
onStateChange(next)
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkAsRead = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READ)) {
|
||||
message = "Marked ${item.title} as read."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkAsUnread = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) {
|
||||
message = "Marked ${item.title} as unread."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onNotInterested = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NOT_INTERESTED)) {
|
||||
message = "Marked ${item.title} as not interesting."
|
||||
refresh()
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val deleted = runCatching { deleteLibraryItem(item.fileId) }.getOrDefault(false)
|
||||
message = if (deleted) "Removed ${item.title}." else "Could not remove ${item.title}."
|
||||
if (deleted) {
|
||||
items = items.filterNot { it.fileId == item.fileId }
|
||||
searchResults = searchResults.filterNot { it.fileId == item.fileId }
|
||||
coverCache.remove(item.fileId)
|
||||
nextOffset = (nextOffset - 1).coerceAtLeast(items.size)
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!searchActive && !endReached) {
|
||||
item(key = "load-more") {
|
||||
@@ -482,15 +532,56 @@ private fun EmptySearchPane(modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.librarySection(
|
||||
key: String,
|
||||
title: String,
|
||||
count: Int,
|
||||
collapsed: Boolean,
|
||||
onCollapsedChange: (Boolean) -> Unit,
|
||||
content: LazyListScope.() -> Unit,
|
||||
) {
|
||||
if (count == 0) return
|
||||
item(key = "section-$key") {
|
||||
LibrarySectionHeader(
|
||||
text = title,
|
||||
count = count,
|
||||
collapsed = collapsed,
|
||||
onToggle = { onCollapsedChange(!collapsed) },
|
||||
)
|
||||
}
|
||||
if (!collapsed) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibrarySectionHeader(text: String) {
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 4.dp),
|
||||
)
|
||||
private fun LibrarySectionHeader(
|
||||
text: String,
|
||||
count: Int,
|
||||
collapsed: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(top = 10.dp, bottom = 4.dp, start = 8.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
if (collapsed) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Filled.KeyboardArrowDown,
|
||||
contentDescription = if (collapsed) "Expand $text" else "Collapse $text",
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Text(
|
||||
"$text ($count)",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -528,11 +619,7 @@ private fun LibraryRow(
|
||||
item: LibraryItem,
|
||||
coverCache: MutableMap<String, LibraryCover?>,
|
||||
enabled: Boolean,
|
||||
onOpen: () -> Unit,
|
||||
onMarkAsRead: () -> Unit,
|
||||
onMarkAsUnread: () -> Unit,
|
||||
onNotInterested: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
actions: LibraryItemActions,
|
||||
) {
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -540,7 +627,7 @@ private fun LibraryRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled, onClick = onOpen)
|
||||
.clickable(enabled = enabled, onClick = actions.onOpen)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -575,7 +662,7 @@ private fun LibraryRow(
|
||||
text = { Text("Open") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onOpen()
|
||||
actions.onOpen()
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
@@ -584,7 +671,7 @@ private fun LibraryRow(
|
||||
text = { Text("Mark as read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onMarkAsRead()
|
||||
actions.onMarkAsRead()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -593,15 +680,24 @@ private fun LibraryRow(
|
||||
text = { Text("Mark as unread") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onMarkAsUnread()
|
||||
actions.onMarkAsUnread()
|
||||
},
|
||||
)
|
||||
}
|
||||
if (item.readingStatus != BookReadingStatus.NEW) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Remove marks") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onRemoveMarks()
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Not interesting") },
|
||||
text = { Text("Not interested") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onNotInterested()
|
||||
actions.onNotInterested()
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
@@ -609,7 +705,7 @@ private fun LibraryRow(
|
||||
text = { Text("Delete") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onDelete()
|
||||
actions.onDelete()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -618,6 +714,15 @@ private fun LibraryRow(
|
||||
}
|
||||
}
|
||||
|
||||
private data class LibraryItemActions(
|
||||
val onOpen: () -> Unit,
|
||||
val onMarkAsRead: () -> Unit,
|
||||
val onMarkAsUnread: () -> Unit,
|
||||
val onRemoveMarks: () -> Unit,
|
||||
val onNotInterested: () -> Unit,
|
||||
val onDelete: () -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun LibraryCover(
|
||||
item: LibraryItem,
|
||||
|
||||
Reference in new issue
Block a user