Merge feature/cache-ui: RemoteLibrary cache UI

This commit is contained in:
Atte149 2026-06-16 12:05:45 +03:00
commit 7205e35d9d

View file

@ -8,7 +8,9 @@ package org.dueattendant149.bookshelf.ui.bookshelf
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -19,15 +21,21 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.CloudQueue import androidx.compose.material.icons.filled.CloudQueue
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -39,6 +47,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.dueattendant149.bookshelf.R import org.dueattendant149.bookshelf.R
import org.dueattendant149.bookshelf.domain.model.cache.CacheState
import org.dueattendant149.bookshelf.domain.model.cache.CacheStatus
import org.dueattendant149.bookshelf.domain.model.library.Book
import org.dueattendant149.bookshelf.presentation.bookshelf.RemoteLibraryModel import org.dueattendant149.bookshelf.presentation.bookshelf.RemoteLibraryModel
import org.dueattendant149.bookshelf.ui.common.components.placeholder.EmptyPlaceholder import org.dueattendant149.bookshelf.ui.common.components.placeholder.EmptyPlaceholder
import org.dueattendant149.bookshelf.ui.common.components.placeholder.ErrorPlaceholder import org.dueattendant149.bookshelf.ui.common.components.placeholder.ErrorPlaceholder
@ -125,28 +136,34 @@ fun RemoteLibraryContent(model: RemoteLibraryModel) {
modifier = Modifier.padding(bottom = 4.dp) modifier = Modifier.padding(bottom = 4.dp)
) )
} }
item {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.bookshelf_offline_only_label),
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = state.offlineOnly,
onCheckedChange = { model.toggleOfflineOnly() }
)
}
}
} }
items(state.books.size) { index -> items(state.books.size) { index ->
val book = state.books[index] val book = state.books[index]
Card( val cacheStatus = state.cacheStatuses[book.remoteId]
modifier = Modifier.fillMaxWidth(), BookListItem(
) { book = book,
ListItem( cacheStatus = cacheStatus,
headlineContent = { onDownload = { model.cacheBook(book) },
Text( onDelete = { model.deleteCache(book) }
text = book.title,
style = MaterialTheme.typography.titleMedium
) )
},
supportingContent = {
Text(
text = book.author.getAsString()
?: stringResource(R.string.unknown_author)
)
}
)
}
} }
if (state.isLoading && state.libraries.isEmpty() && state.books.isEmpty()) { if (state.isLoading && state.libraries.isEmpty() && state.books.isEmpty()) {
@ -193,3 +210,84 @@ fun RemoteLibraryContent(model: RemoteLibraryModel) {
} }
} }
} }
@Composable
private fun BookListItem(
book: Book,
cacheStatus: CacheStatus?,
onDownload: () -> Unit,
onDelete: () -> Unit,
) {
val cacheState = cacheStatus?.state ?: CacheState.NONE
val showDownload = cacheState == CacheState.NONE || cacheState == CacheState.FAILED
val showDelete = cacheState != CacheState.NONE
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column {
ListItem(
headlineContent = {
Text(
text = book.title,
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = {
Column {
Text(
text = book.author.getAsString()
?: stringResource(R.string.unknown_author)
)
if (cacheState != CacheState.NONE) {
Text(
text = cacheStatusLabel(cacheState),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
},
trailingContent = {
Row {
if (showDownload) {
IconButton(onClick = onDownload) {
Icon(
imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.bookshelf_download_action)
)
}
}
if (showDelete) {
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.bookshelf_delete_cache_action)
)
}
}
}
}
)
if (cacheState == CacheState.DOWNLOADING) {
LinearProgressIndicator(
progress = { cacheStatus?.progress?.coerceIn(0f, 1f) ?: 0f },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}
}
}
@Composable
private fun cacheStatusLabel(state: CacheState): String =
when (state) {
CacheState.NONE -> ""
CacheState.PENDING -> stringResource(R.string.bookshelf_cache_pending)
CacheState.DOWNLOADING -> stringResource(R.string.bookshelf_cache_downloading)
CacheState.COMPLETED -> stringResource(R.string.bookshelf_cache_completed)
CacheState.FAILED -> stringResource(R.string.bookshelf_cache_failed)
}