Add library categories and favorites
This commit is contained in:
parent
d749352333
commit
02bde7e644
@ -337,6 +337,12 @@ actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingS
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boolean = withContext(Dispatchers.IO) {
|
||||
openLibraryDatabase().useLibrary { db ->
|
||||
db.files.updateFavorite(fileId, favorite)
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val shareFile = openLibraryDatabase().useLibrary { db ->
|
||||
@ -698,6 +704,7 @@ private fun LibraryFileRecord.toLibraryItem(): LibraryItem =
|
||||
storageUri = storageUri,
|
||||
lastSeenAt = lastSeenAt,
|
||||
readingStatus = readingStatus,
|
||||
favorite = favorite,
|
||||
lastReadAt = lastReadAt,
|
||||
importedAt = importedAt,
|
||||
)
|
||||
|
||||
@ -16,6 +16,7 @@ data class LibraryItem(
|
||||
val storageUri: String?,
|
||||
val lastSeenAt: Long?,
|
||||
val readingStatus: BookReadingStatus = BookReadingStatus.NEW,
|
||||
val favorite: Boolean = false,
|
||||
val lastReadAt: Long? = null,
|
||||
val importedAt: Long? = null,
|
||||
val coverImage: ByteArray? = null,
|
||||
@ -124,6 +125,8 @@ expect suspend fun clearLibraryReadingPosition(fileId: String)
|
||||
|
||||
expect suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean
|
||||
|
||||
expect suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boolean
|
||||
|
||||
expect suspend fun shareLibraryBookFile(fileId: String): Boolean
|
||||
|
||||
expect suspend fun viewLibraryBookFile(fileId: String): Boolean
|
||||
|
||||
@ -25,6 +25,7 @@ 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.Favorite
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
@ -53,6 +54,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
@ -60,6 +62,7 @@ import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -97,10 +100,14 @@ internal fun LibraryScreen(
|
||||
var settingsMenuOpen by remember { mutableStateOf(false) }
|
||||
var autoScanDownloads by remember { mutableStateOf(true) }
|
||||
var autoScanSettingLoaded by remember { mutableStateOf(false) }
|
||||
var backgroundDownloadsRescanStarted by remember { mutableStateOf(false) }
|
||||
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 favoritesCollapsed by remember { mutableStateOf(false) }
|
||||
var toReadCollapsed by remember { mutableStateOf(false) }
|
||||
var readCollapsed by remember { mutableStateOf(false) }
|
||||
var recentlyAddedCollapsed by remember { mutableStateOf(false) }
|
||||
var myLibraryCollapsed by remember { mutableStateOf(false) }
|
||||
var notInterestedCollapsed by remember { mutableStateOf(false) }
|
||||
@ -171,6 +178,31 @@ internal fun LibraryScreen(
|
||||
val updatedItem = loadLibraryItem(item.fileId) ?: item.copy(readingStatus = status)
|
||||
items = items.replaceLibraryItem(updatedItem)
|
||||
searchResults = searchResults.replaceLibraryItem(updatedItem)
|
||||
recentlyAddedItems = recentlyAddedItems.replaceLibraryItem(updatedItem)
|
||||
message = successMessage
|
||||
if (searchActive) {
|
||||
searching = true
|
||||
searchResults = searchLibraryItems(searchText, SearchResultLimit)
|
||||
searching = false
|
||||
} else {
|
||||
loadPage(reset = true)
|
||||
loadRecentlyAdded()
|
||||
}
|
||||
} else {
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun applyFavorite(
|
||||
item: LibraryItem,
|
||||
favorite: Boolean,
|
||||
successMessage: String,
|
||||
) {
|
||||
if (markLibraryFavorite(item.fileId, favorite)) {
|
||||
val updatedItem = loadLibraryItem(item.fileId) ?: item.copy(favorite = favorite)
|
||||
items = items.replaceLibraryItem(updatedItem)
|
||||
searchResults = searchResults.replaceLibraryItem(updatedItem)
|
||||
recentlyAddedItems = recentlyAddedItems.replaceLibraryItem(updatedItem)
|
||||
message = successMessage
|
||||
if (searchActive) {
|
||||
searching = true
|
||||
@ -246,10 +278,18 @@ internal fun LibraryScreen(
|
||||
autoScanSettingLoaded = true
|
||||
}
|
||||
|
||||
LaunchedEffect(autoScanSettingLoaded, autoScanDownloads) {
|
||||
if (!autoScanSettingLoaded || !autoScanDownloads || activeScan != null) return@LaunchedEffect
|
||||
LaunchedEffect(autoScanSettingLoaded, autoScanDownloads, activeScan == null) {
|
||||
if (
|
||||
!autoScanSettingLoaded ||
|
||||
!autoScanDownloads ||
|
||||
backgroundDownloadsRescanStarted ||
|
||||
activeScan != null
|
||||
) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
if (loadDownloadsWasScanned()) {
|
||||
downloadsScanPath()?.let { path ->
|
||||
backgroundDownloadsRescanStarted = true
|
||||
message = "Scanning Downloads..."
|
||||
onStartScan(path)
|
||||
}
|
||||
@ -260,7 +300,7 @@ internal fun LibraryScreen(
|
||||
if (activeScan != null) {
|
||||
wasScanning = true
|
||||
while (true) {
|
||||
delay(5_000)
|
||||
delay(2_000)
|
||||
loadPage(reset = true)
|
||||
loadRecentlyAdded()
|
||||
}
|
||||
@ -364,7 +404,9 @@ internal fun LibraryScreen(
|
||||
readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem)
|
||||
coverCache[updatedItem.fileId] = loadLibraryItemCover(updatedItem.fileId)
|
||||
}
|
||||
if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)) {
|
||||
if (item.readingStatus == BookReadingStatus.NEW &&
|
||||
markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)
|
||||
) {
|
||||
val readingItem = loadLibraryItem(item.fileId)
|
||||
?: item.copy(readingStatus = BookReadingStatus.READING)
|
||||
items = items.replaceLibraryItem(readingItem)
|
||||
@ -418,6 +460,16 @@ internal fun LibraryScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkToRead = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
applyReadingStatus(item, BookReadingStatus.TO_READ, "Marked ${item.title} to read.")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onNotInterested = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
@ -428,6 +480,17 @@ internal fun LibraryScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteChange = { favorite ->
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val label = if (favorite) "Added ${item.title} to favorites." else "Removed ${item.title} from favorites."
|
||||
applyFavorite(item, favorite, label)
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
val previousItems = items
|
||||
val previousSearchResults = searchResults
|
||||
@ -473,6 +536,9 @@ internal fun LibraryScreen(
|
||||
libraryRows("search", visibleItems)
|
||||
} else {
|
||||
val readingNow = visibleItems.filter { it.readingStatus == BookReadingStatus.READING }
|
||||
val favorites = visibleItems.filter { it.favorite }
|
||||
val toRead = visibleItems.filter { it.readingStatus == BookReadingStatus.TO_READ }
|
||||
val read = visibleItems.filter { it.readingStatus == BookReadingStatus.READ }
|
||||
val recentlyAdded = recentlyAddedItems.filter {
|
||||
it.fileId !in hiddenFileIds && it.readingStatus == BookReadingStatus.NEW
|
||||
}
|
||||
@ -480,6 +546,8 @@ internal fun LibraryScreen(
|
||||
val myLibrary = visibleItems.filter {
|
||||
it.fileId !in recentlyAddedIds &&
|
||||
it.readingStatus != BookReadingStatus.READING &&
|
||||
it.readingStatus != BookReadingStatus.TO_READ &&
|
||||
it.readingStatus != BookReadingStatus.READ &&
|
||||
it.readingStatus != BookReadingStatus.NOT_INTERESTED
|
||||
}
|
||||
val notInterested = visibleItems.filter { it.readingStatus == BookReadingStatus.NOT_INTERESTED }
|
||||
@ -494,6 +562,26 @@ internal fun LibraryScreen(
|
||||
) {
|
||||
libraryRows("reading", readingNow)
|
||||
}
|
||||
librarySection(
|
||||
key = "favorites",
|
||||
title = "favorites",
|
||||
itemCount = favorites.size,
|
||||
displayCount = favorites.size.takeIf { endReached },
|
||||
collapsed = favoritesCollapsed,
|
||||
onCollapsedChange = { favoritesCollapsed = it },
|
||||
) {
|
||||
libraryRows("favorites", favorites)
|
||||
}
|
||||
librarySection(
|
||||
key = "to-read",
|
||||
title = "to read",
|
||||
itemCount = toRead.size,
|
||||
displayCount = toRead.size.takeIf { endReached },
|
||||
collapsed = toReadCollapsed,
|
||||
onCollapsedChange = { toReadCollapsed = it },
|
||||
) {
|
||||
libraryRows("to-read", toRead)
|
||||
}
|
||||
librarySection(
|
||||
key = "recently-added",
|
||||
title = "recently added",
|
||||
@ -514,6 +602,16 @@ internal fun LibraryScreen(
|
||||
) {
|
||||
libraryRows("library", myLibrary)
|
||||
}
|
||||
librarySection(
|
||||
key = "read",
|
||||
title = "read",
|
||||
itemCount = read.size,
|
||||
displayCount = read.size.takeIf { endReached },
|
||||
collapsed = readCollapsed,
|
||||
onCollapsedChange = { readCollapsed = it },
|
||||
) {
|
||||
libraryRows("read", read)
|
||||
}
|
||||
librarySection(
|
||||
key = "not-interested",
|
||||
title = "not interested",
|
||||
@ -723,6 +821,8 @@ private fun LibraryRow(
|
||||
actions: LibraryItemActions,
|
||||
) {
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
val titleStyle = MaterialTheme.typography.titleMedium
|
||||
val favoriteIconSize = with(LocalDensity.current) { titleStyle.lineHeight.toDp() - 2.dp }
|
||||
|
||||
Card(shape = RoundedCornerShape(6.dp), colors = quietCardColors(), modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
@ -735,13 +835,27 @@ private fun LibraryRow(
|
||||
) {
|
||||
LibraryCover(item, coverCache, modifier = Modifier.width(46.dp).aspectRatio(0.68f))
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
item.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
item.title,
|
||||
style = titleStyle,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (item.favorite) {
|
||||
Icon(
|
||||
Icons.Filled.Favorite,
|
||||
contentDescription = "Favorite",
|
||||
tint = Color(0xFFD32F2F),
|
||||
modifier = Modifier.size(favoriteIconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
item.authors.joinToString().ifBlank { "Unknown author" },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@ -787,15 +901,31 @@ private fun LibraryRow(
|
||||
},
|
||||
)
|
||||
}
|
||||
if (item.readingStatus != BookReadingStatus.TO_READ) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Mark to read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onMarkToRead()
|
||||
},
|
||||
)
|
||||
}
|
||||
if (item.readingStatus != BookReadingStatus.NEW) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Remove marks") },
|
||||
text = { Text(if (item.readingStatus == BookReadingStatus.TO_READ) "Remove to read" else "Remove marks") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onRemoveMarks()
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (item.favorite) "Remove favorite" else "Add favorite") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onFavoriteChange(!item.favorite)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Not interested") },
|
||||
onClick = {
|
||||
@ -822,7 +952,9 @@ private data class LibraryItemActions(
|
||||
val onMarkAsRead: () -> Unit,
|
||||
val onMarkAsUnread: () -> Unit,
|
||||
val onRemoveMarks: () -> Unit,
|
||||
val onMarkToRead: () -> Unit,
|
||||
val onNotInterested: () -> Unit,
|
||||
val onFavoriteChange: (Boolean) -> Unit,
|
||||
val onDelete: () -> Unit,
|
||||
)
|
||||
|
||||
@ -866,6 +998,7 @@ private fun LibraryCover(
|
||||
|
||||
private fun LibraryItem.libraryMetadataLine(): String =
|
||||
listOfNotNull(
|
||||
"Favorite".takeIf { favorite },
|
||||
readingStatus.displayLabel,
|
||||
lastReadAt?.formatLastRead(),
|
||||
date?.yearOrRaw(),
|
||||
@ -877,6 +1010,7 @@ private fun LibraryItem.libraryMetadataLine(): String =
|
||||
private val BookReadingStatus.displayLabel: String
|
||||
get() = when (this) {
|
||||
BookReadingStatus.NEW -> "New"
|
||||
BookReadingStatus.TO_READ -> "To read"
|
||||
BookReadingStatus.READING -> "Reading"
|
||||
BookReadingStatus.READ -> "Read"
|
||||
BookReadingStatus.NOT_INTERESTED -> "Not interested"
|
||||
|
||||
@ -81,6 +81,7 @@ internal fun BookView(
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var restored by remember(fileId) { mutableStateOf(false) }
|
||||
var markedRead by remember(fileId) { mutableStateOf(false) }
|
||||
var libraryItem by remember(fileId) { mutableStateOf<LibraryItem?>(null) }
|
||||
var readAloudPanelVisible by remember(fileId) { mutableStateOf(false) }
|
||||
var readAloudSettingsVisible by remember(fileId) { mutableStateOf(false) }
|
||||
val readAloudState by ReadAloudPlatform.state.collectAsState()
|
||||
@ -102,6 +103,7 @@ internal fun BookView(
|
||||
fun setReadingStatus(status: BookReadingStatus, successMessage: String) {
|
||||
scope.launch {
|
||||
if (markLibraryReadingStatus(fileId, status)) {
|
||||
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = status)
|
||||
if (status == BookReadingStatus.READ) markedRead = true
|
||||
if (status == BookReadingStatus.NEW) markedRead = false
|
||||
showMessage(successMessage)
|
||||
@ -112,7 +114,12 @@ internal fun BookView(
|
||||
}
|
||||
|
||||
LaunchedEffect(fileId) {
|
||||
markLibraryReadingStatus(fileId, BookReadingStatus.READING)
|
||||
val item = loadLibraryItem(fileId)
|
||||
libraryItem = item
|
||||
markedRead = item?.readingStatus == BookReadingStatus.READ
|
||||
if (item?.readingStatus == BookReadingStatus.NEW && markLibraryReadingStatus(fileId, BookReadingStatus.READING)) {
|
||||
libraryItem = loadLibraryItem(fileId) ?: item.copy(readingStatus = BookReadingStatus.READING)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(fileId) {
|
||||
@ -144,10 +151,12 @@ internal fun BookView(
|
||||
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
layoutInfo.totalItemsCount > 0 && lastVisible >= layoutInfo.totalItemsCount - 1
|
||||
}
|
||||
.filter { restored && it && !markedRead }
|
||||
.filter { restored && it && !markedRead && libraryItem?.readingStatus != BookReadingStatus.TO_READ }
|
||||
.collect {
|
||||
markedRead = true
|
||||
markLibraryReadingStatus(fileId, BookReadingStatus.READ)
|
||||
if (markLibraryReadingStatus(fileId, BookReadingStatus.READ)) {
|
||||
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = BookReadingStatus.READ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,12 +188,27 @@ internal fun BookView(
|
||||
onMarkAsRead = {
|
||||
setReadingStatus(BookReadingStatus.READ, "Marked as read.")
|
||||
},
|
||||
onMarkToRead = {
|
||||
setReadingStatus(BookReadingStatus.TO_READ, "Marked to read.")
|
||||
},
|
||||
onNotInterested = {
|
||||
setReadingStatus(BookReadingStatus.NOT_INTERESTED, "Marked as not interested.")
|
||||
},
|
||||
onClearMarks = {
|
||||
setReadingStatus(BookReadingStatus.NEW, "Cleared marks.")
|
||||
},
|
||||
readingStatus = libraryItem?.readingStatus,
|
||||
favorite = libraryItem?.favorite == true,
|
||||
onFavoriteChange = { favorite ->
|
||||
scope.launch {
|
||||
if (markLibraryFavorite(fileId, favorite)) {
|
||||
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(favorite = favorite)
|
||||
showMessage(if (favorite) "Added to favorites." else "Removed from favorites.")
|
||||
} else {
|
||||
showMessage("Could not update book.")
|
||||
}
|
||||
}
|
||||
},
|
||||
showShareAction = showShareAction,
|
||||
onShare = {
|
||||
scope.launch {
|
||||
@ -290,8 +314,12 @@ private fun CompactReaderTopBar(
|
||||
onThemeToggle: () -> Unit,
|
||||
onBookInfo: () -> Unit,
|
||||
onMarkAsRead: () -> Unit,
|
||||
onMarkToRead: () -> Unit,
|
||||
onNotInterested: () -> Unit,
|
||||
onClearMarks: () -> Unit,
|
||||
readingStatus: BookReadingStatus?,
|
||||
favorite: Boolean,
|
||||
onFavoriteChange: (Boolean) -> Unit,
|
||||
showShareAction: Boolean,
|
||||
onShare: () -> Unit,
|
||||
showViewFileAction: Boolean,
|
||||
@ -345,6 +373,23 @@ private fun CompactReaderTopBar(
|
||||
onMarkAsRead()
|
||||
},
|
||||
)
|
||||
if (readingStatus == BookReadingStatus.TO_READ) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Remove to read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onClearMarks()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Mark to read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onMarkToRead()
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Not interested") },
|
||||
onClick = {
|
||||
@ -359,6 +404,13 @@ private fun CompactReaderTopBar(
|
||||
onClearMarks()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (favorite) "Remove favorite" else "Add favorite") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onFavoriteChange(!favorite)
|
||||
},
|
||||
)
|
||||
if (showShareAction || showViewFileAction) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
@ -285,6 +285,12 @@ actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingS
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boolean = withContext(Dispatchers.IO) {
|
||||
openLibraryDatabase().useLibrary { db ->
|
||||
db.files.updateFavorite(fileId, favorite)
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = withContext(Dispatchers.IO) {
|
||||
@ -495,6 +501,7 @@ private fun LibraryFileRecord.toLibraryItem(): LibraryItem =
|
||||
storageUri = storageUri,
|
||||
lastSeenAt = lastSeenAt,
|
||||
readingStatus = readingStatus,
|
||||
favorite = favorite,
|
||||
lastReadAt = lastReadAt,
|
||||
importedAt = importedAt,
|
||||
)
|
||||
|
||||
@ -57,6 +57,8 @@ actual suspend fun clearLibraryReadingPosition(fileId: String) = Unit
|
||||
|
||||
actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = false
|
||||
|
||||
actual suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boolean = false
|
||||
|
||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
@ -17,6 +17,7 @@ enum class BookImportPolicy {
|
||||
|
||||
enum class BookReadingStatus {
|
||||
NEW,
|
||||
TO_READ,
|
||||
READING,
|
||||
READ,
|
||||
NOT_INTERESTED,
|
||||
@ -109,6 +110,7 @@ data class BookFileRecord(
|
||||
val lastModifiedMillis: Long? = null,
|
||||
val lastSeenAt: Long? = null,
|
||||
val readingStatus: BookReadingStatus = BookReadingStatus.NEW,
|
||||
val favorite: Boolean = false,
|
||||
val lastReadAt: Long? = null,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long,
|
||||
@ -128,6 +130,7 @@ data class LibraryFileRecord(
|
||||
val storageUri: String? = null,
|
||||
val lastSeenAt: Long? = null,
|
||||
val readingStatus: BookReadingStatus = BookReadingStatus.NEW,
|
||||
val favorite: Boolean = false,
|
||||
val lastReadAt: Long? = null,
|
||||
val importedAt: Long,
|
||||
)
|
||||
@ -210,6 +213,7 @@ interface BookFileRepository {
|
||||
fun list(limit: Int = 500, offset: Int = 0): List<BookFileRecord>
|
||||
fun listForBook(bookId: String): List<BookFileRecord>
|
||||
fun updateReadingStatus(id: String, status: BookReadingStatus): Boolean
|
||||
fun updateFavorite(id: String, favorite: Boolean): Boolean
|
||||
fun touchLastReadAt(id: String, lastReadAt: Long): Boolean
|
||||
fun delete(id: String): Boolean
|
||||
}
|
||||
|
||||
@ -213,6 +213,7 @@ private fun migrate(connection: Connection) {
|
||||
last_modified_millis BIGINT,
|
||||
last_seen_at BIGINT,
|
||||
reading_status VARCHAR NOT NULL DEFAULT 'NEW',
|
||||
favorite BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
last_read_at BIGINT,
|
||||
imported_at BIGINT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
@ -225,6 +226,7 @@ private fun migrate(connection: Connection) {
|
||||
)
|
||||
statement.execute("ALTER TABLE book_files ADD COLUMN IF NOT EXISTS duplicate_of_file_id VARCHAR")
|
||||
statement.execute("ALTER TABLE book_files ADD COLUMN IF NOT EXISTS reading_status VARCHAR NOT NULL DEFAULT 'NEW'")
|
||||
statement.execute("ALTER TABLE book_files ADD COLUMN IF NOT EXISTS favorite BOOLEAN NOT NULL DEFAULT FALSE")
|
||||
statement.execute("ALTER TABLE book_files ADD COLUMN IF NOT EXISTS last_read_at BIGINT")
|
||||
statement.execute("ALTER TABLE book_files ADD COLUMN IF NOT EXISTS imported_at BIGINT")
|
||||
connection.prepareStatement("UPDATE book_files SET imported_at = ? WHERE imported_at IS NULL").use { update ->
|
||||
@ -487,9 +489,9 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
MERGE INTO book_files(
|
||||
id, book_id, body_id, body_cluster_id, duplicate_of_file_id, raw_sha256, format,
|
||||
mime_type, size_bytes, original_filename, storage_kind, storage_uri, content_object_id,
|
||||
last_modified_millis, last_seen_at, reading_status, last_read_at, imported_at, created_at, updated_at
|
||||
last_modified_millis, last_seen_at, reading_status, favorite, last_read_at, imported_at, created_at, updated_at
|
||||
)
|
||||
KEY(id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
KEY(id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""".trimIndent()
|
||||
).use { statement ->
|
||||
statement.setString(1, file.id)
|
||||
@ -508,10 +510,11 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
statement.setLongOrNull(14, file.lastModifiedMillis)
|
||||
statement.setLongOrNull(15, file.lastSeenAt)
|
||||
statement.setString(16, file.readingStatus.name)
|
||||
statement.setLongOrNull(17, file.lastReadAt)
|
||||
statement.setLong(18, file.importedAt)
|
||||
statement.setLong(19, file.createdAt)
|
||||
statement.setLong(20, file.updatedAt)
|
||||
statement.setBoolean(17, file.favorite)
|
||||
statement.setLongOrNull(18, file.lastReadAt)
|
||||
statement.setLong(19, file.importedAt)
|
||||
statement.setLong(20, file.createdAt)
|
||||
statement.setLong(21, file.updatedAt)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
@ -536,6 +539,7 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
f.storage_uri AS storage_uri,
|
||||
f.last_seen_at AS last_seen_at,
|
||||
f.reading_status AS reading_status,
|
||||
f.favorite AS favorite,
|
||||
f.last_read_at AS last_read_at,
|
||||
f.imported_at AS imported_at
|
||||
FROM book_files f
|
||||
@ -692,6 +696,7 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
f.storage_uri AS storage_uri,
|
||||
f.last_seen_at AS last_seen_at,
|
||||
f.reading_status AS reading_status,
|
||||
f.favorite AS favorite,
|
||||
f.last_read_at AS last_read_at,
|
||||
f.imported_at AS imported_at
|
||||
FROM book_files f
|
||||
@ -732,6 +737,7 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
f.storage_uri AS storage_uri,
|
||||
f.last_seen_at AS last_seen_at,
|
||||
f.reading_status AS reading_status,
|
||||
f.favorite AS favorite,
|
||||
f.last_read_at AS last_read_at,
|
||||
f.imported_at AS imported_at
|
||||
FROM book_files f
|
||||
@ -771,6 +777,7 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
f.storage_uri AS storage_uri,
|
||||
f.last_seen_at AS last_seen_at,
|
||||
f.reading_status AS reading_status,
|
||||
f.favorite AS favorite,
|
||||
f.last_read_at AS last_read_at,
|
||||
f.imported_at AS imported_at
|
||||
FROM book_files f
|
||||
@ -847,6 +854,24 @@ private class JdbcBookFileRepository(private val connection: Connection) : BookF
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateFavorite(id: String, favorite: Boolean): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
return connection.prepareStatement(
|
||||
"""
|
||||
UPDATE book_files
|
||||
SET favorite = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
OR body_cluster_id = (SELECT body_cluster_id FROM book_files WHERE id = ? AND body_cluster_id IS NOT NULL)
|
||||
""".trimIndent()
|
||||
).use { statement ->
|
||||
statement.setBoolean(1, favorite)
|
||||
statement.setLong(2, now)
|
||||
statement.setString(3, id)
|
||||
statement.setString(4, id)
|
||||
statement.executeUpdate() > 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun touchLastReadAt(id: String, lastReadAt: Long): Boolean {
|
||||
return connection.prepareStatement(
|
||||
"""
|
||||
@ -1085,6 +1110,7 @@ private fun ResultSet.toLibraryFileRecord() = LibraryFileRecord(
|
||||
storageUri = getString("storage_uri"),
|
||||
lastSeenAt = getLongOrNull("last_seen_at"),
|
||||
readingStatus = getReadingStatus("reading_status"),
|
||||
favorite = getBoolean("favorite"),
|
||||
lastReadAt = getLongOrNull("last_read_at"),
|
||||
importedAt = getLong("imported_at"),
|
||||
)
|
||||
@ -1122,6 +1148,7 @@ private fun ResultSet.toBookFileRecord() = BookFileRecord(
|
||||
lastModifiedMillis = getLongOrNull("last_modified_millis"),
|
||||
lastSeenAt = getLongOrNull("last_seen_at"),
|
||||
readingStatus = getReadingStatus("reading_status"),
|
||||
favorite = getBoolean("favorite"),
|
||||
lastReadAt = getLongOrNull("last_read_at"),
|
||||
importedAt = getLong("imported_at"),
|
||||
createdAt = getLong("created_at"),
|
||||
|
||||
@ -122,6 +122,9 @@ class H2LibraryDatabaseTest {
|
||||
assertEquals("2024", db.files.getLibraryFile("file-1")?.date)
|
||||
assertEquals("image/jpeg", db.books.getCover("book-1")?.mimeType)
|
||||
assertEquals(BookReadingStatus.NEW, db.files.getLibraryFile("file-1")?.readingStatus)
|
||||
assertEquals(false, db.files.getLibraryFile("file-1")?.favorite)
|
||||
assertEquals(true, db.files.updateFavorite("file-1", true))
|
||||
assertEquals(true, db.files.getLibraryFile("file-1")?.favorite)
|
||||
assertEquals(true, db.files.updateReadingStatus("file-1", BookReadingStatus.READING))
|
||||
assertEquals(BookReadingStatus.READING, db.files.getLibraryFile("file-1")?.readingStatus)
|
||||
assertEquals("body-1", db.bodies.findByExactTextHash("text-sha", 1)?.id)
|
||||
@ -299,6 +302,9 @@ class H2LibraryDatabaseTest {
|
||||
assertEquals(true, db.files.updateReadingStatus("file-zip", BookReadingStatus.READING))
|
||||
assertEquals(BookReadingStatus.READING, db.files.get("file-fb2")?.readingStatus)
|
||||
assertEquals(BookReadingStatus.READING, db.files.get("file-zip")?.readingStatus)
|
||||
assertEquals(true, db.files.updateFavorite("file-zip", true))
|
||||
assertEquals(true, db.files.get("file-fb2")?.favorite)
|
||||
assertEquals(true, db.files.get("file-zip")?.favorite)
|
||||
db.close()
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user