mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
feat: add genre picker functionality to collection editor
This commit is contained in:
parent
7727436272
commit
9902bbd3ef
2 changed files with 536 additions and 313 deletions
|
|
@ -23,6 +23,7 @@ data class CollectionEditorUiState(
|
||||||
val editingFolder: CollectionFolder? = null,
|
val editingFolder: CollectionFolder? = null,
|
||||||
val showFolderEditor: Boolean = false,
|
val showFolderEditor: Boolean = false,
|
||||||
val showCatalogPicker: Boolean = false,
|
val showCatalogPicker: Boolean = false,
|
||||||
|
val genrePickerSourceIndex: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
object CollectionEditorRepository {
|
object CollectionEditorRepository {
|
||||||
|
|
@ -224,6 +225,7 @@ object CollectionEditorRepository {
|
||||||
editingFolder = folder.copy(
|
editingFolder = folder.copy(
|
||||||
catalogSources = folder.catalogSources.toMutableList().apply { removeAt(index) },
|
catalogSources = folder.catalogSources.toMutableList().apply { removeAt(index) },
|
||||||
),
|
),
|
||||||
|
genrePickerSourceIndex = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,13 +252,29 @@ object CollectionEditorRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCatalogPicker() {
|
fun showCatalogPicker() {
|
||||||
_uiState.value = _uiState.value.copy(showCatalogPicker = true)
|
_uiState.value = _uiState.value.copy(
|
||||||
|
showCatalogPicker = true,
|
||||||
|
genrePickerSourceIndex = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideCatalogPicker() {
|
fun hideCatalogPicker() {
|
||||||
_uiState.value = _uiState.value.copy(showCatalogPicker = false)
|
_uiState.value = _uiState.value.copy(showCatalogPicker = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showGenrePicker(index: Int) {
|
||||||
|
val folder = _uiState.value.editingFolder ?: return
|
||||||
|
if (index !in folder.catalogSources.indices) return
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
genrePickerSourceIndex = index,
|
||||||
|
showCatalogPicker = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideGenrePicker() {
|
||||||
|
_uiState.value = _uiState.value.copy(genrePickerSourceIndex = null)
|
||||||
|
}
|
||||||
|
|
||||||
fun saveFolderEdit() {
|
fun saveFolderEdit() {
|
||||||
val folder = _uiState.value.editingFolder ?: return
|
val folder = _uiState.value.editingFolder ?: return
|
||||||
val existing = _uiState.value.folders
|
val existing = _uiState.value.folders
|
||||||
|
|
@ -270,6 +288,7 @@ object CollectionEditorRepository {
|
||||||
editingFolder = null,
|
editingFolder = null,
|
||||||
showFolderEditor = false,
|
showFolderEditor = false,
|
||||||
showCatalogPicker = false,
|
showCatalogPicker = false,
|
||||||
|
genrePickerSourceIndex = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,6 +297,7 @@ object CollectionEditorRepository {
|
||||||
editingFolder = null,
|
editingFolder = null,
|
||||||
showFolderEditor = false,
|
showFolderEditor = false,
|
||||||
showCatalogPicker = false,
|
showCatalogPicker = false,
|
||||||
|
genrePickerSourceIndex = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -36,7 +35,6 @@ import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
|
@ -57,6 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import com.nuvio.app.core.ui.NuvioInputField
|
import com.nuvio.app.core.ui.NuvioInputField
|
||||||
|
import com.nuvio.app.core.ui.NuvioModalBottomSheet
|
||||||
import com.nuvio.app.core.ui.NuvioPrimaryButton
|
import com.nuvio.app.core.ui.NuvioPrimaryButton
|
||||||
import com.nuvio.app.core.ui.NuvioScreen
|
import com.nuvio.app.core.ui.NuvioScreen
|
||||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||||
|
|
@ -81,11 +80,49 @@ fun CollectionEditorScreen(
|
||||||
CollectionEditorRepository.initialize(collectionId)
|
CollectionEditorRepository.initialize(collectionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showFolderEditor && state.editingFolder != null) {
|
val editingFolder = state.editingFolder
|
||||||
FolderEditorSheet(
|
if (state.showFolderEditor && editingFolder != null) {
|
||||||
|
val genrePickerIndex = state.genrePickerSourceIndex
|
||||||
|
val genrePickerSource = genrePickerIndex?.let { editingFolder.catalogSources.getOrNull(it) }
|
||||||
|
val genrePickerCatalog = genrePickerSource?.let { source ->
|
||||||
|
state.availableCatalogs.find {
|
||||||
|
it.addonId == source.addonId && it.type == source.type && it.catalogId == source.catalogId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderEditorPage(
|
||||||
state = state,
|
state = state,
|
||||||
onDismiss = { CollectionEditorRepository.cancelFolderEdit() },
|
onBack = { CollectionEditorRepository.cancelFolderEdit() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (state.showCatalogPicker) {
|
||||||
|
CatalogPickerSheet(
|
||||||
|
availableCatalogs = state.availableCatalogs,
|
||||||
|
selectedSources = editingFolder.catalogSources,
|
||||||
|
onToggle = { CollectionEditorRepository.toggleCatalogSource(it) },
|
||||||
|
onDismiss = { CollectionEditorRepository.hideCatalogPicker() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
genrePickerIndex != null &&
|
||||||
|
genrePickerSource != null &&
|
||||||
|
genrePickerCatalog != null &&
|
||||||
|
genrePickerCatalog.genreOptions.isNotEmpty()
|
||||||
|
) {
|
||||||
|
GenrePickerSheet(
|
||||||
|
title = genrePickerCatalog.catalogName,
|
||||||
|
selectedGenre = genrePickerSource.genre,
|
||||||
|
genreOptions = genrePickerCatalog.genreOptions,
|
||||||
|
allowAll = !genrePickerCatalog.genreRequired,
|
||||||
|
onSelect = {
|
||||||
|
CollectionEditorRepository.updateCatalogSourceGenre(genrePickerIndex, it)
|
||||||
|
CollectionEditorRepository.hideGenrePicker()
|
||||||
|
},
|
||||||
|
onDismiss = { CollectionEditorRepository.hideGenrePicker() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showCatalogPicker) {
|
if (state.showCatalogPicker) {
|
||||||
|
|
@ -494,52 +531,61 @@ private fun FolderListItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FolderEditorSheet(
|
private fun FolderEditorPage(
|
||||||
state: CollectionEditorUiState,
|
state: CollectionEditorUiState,
|
||||||
onDismiss: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val folder = state.editingFolder ?: return
|
val folder = state.editingFolder ?: return
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val bottomInset = nuvioPlatformExtraBottomPadding
|
||||||
|
|
||||||
ModalBottomSheet(
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
onDismissRequest = onDismiss,
|
NuvioScreen(modifier = Modifier.fillMaxSize()) {
|
||||||
sheetState = sheetState,
|
stickyHeader {
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
NuvioScreenHeader(
|
||||||
) {
|
title = if (state.folders.any { it.id == folder.id }) "Edit Folder" else "New Folder",
|
||||||
LazyColumn(
|
onBack = onBack,
|
||||||
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = "Edit Folder",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
|
NuvioSurfaceCard {
|
||||||
|
Text(
|
||||||
|
text = "Set the folder identity, presentation, and catalog sources with the same structure as the main collections editor.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
FolderEditorSection(title = "BASICS") {
|
||||||
|
NuvioSurfaceCard {
|
||||||
NuvioInputField(
|
NuvioInputField(
|
||||||
value = folder.title,
|
value = folder.title,
|
||||||
onValueChange = { CollectionEditorRepository.updateFolderTitle(it) },
|
onValueChange = { CollectionEditorRepository.updateFolderTitle(it) },
|
||||||
placeholder = "Folder Title",
|
placeholder = "Folder Title",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cover (emoji or image url)
|
|
||||||
item {
|
item {
|
||||||
Column {
|
FolderEditorSection(title = "APPEARANCE") {
|
||||||
|
NuvioSurfaceCard {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Cover",
|
text = "Cover",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
FlowRow(
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = folder.coverEmoji == null && folder.coverImageUrl == null,
|
selected = folder.coverEmoji == null && folder.coverImageUrl == null,
|
||||||
onClick = { CollectionEditorRepository.clearFolderCover() },
|
onClick = { CollectionEditorRepository.clearFolderCover() },
|
||||||
|
|
@ -564,8 +610,9 @@ private fun FolderEditorSheet(
|
||||||
label = { Text("Image") },
|
label = { Text("Image") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (folder.coverEmoji != null) {
|
if (folder.coverEmoji != null) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
NuvioInputField(
|
NuvioInputField(
|
||||||
value = folder.coverEmoji,
|
value = folder.coverEmoji,
|
||||||
onValueChange = { CollectionEditorRepository.updateFolderCoverEmoji(it) },
|
onValueChange = { CollectionEditorRepository.updateFolderCoverEmoji(it) },
|
||||||
|
|
@ -573,60 +620,36 @@ private fun FolderEditorSheet(
|
||||||
modifier = Modifier.width(100.dp),
|
modifier = Modifier.width(100.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folder.coverImageUrl != null) {
|
if (folder.coverImageUrl != null) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
NuvioInputField(
|
NuvioInputField(
|
||||||
value = folder.coverImageUrl,
|
value = folder.coverImageUrl,
|
||||||
onValueChange = { CollectionEditorRepository.updateFolderCoverImage(it) },
|
onValueChange = { CollectionEditorRepository.updateFolderCoverImage(it) },
|
||||||
placeholder = "Image URL",
|
placeholder = "Image URL",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
NuvioInputField(
|
NuvioInputField(
|
||||||
value = folder.focusGifUrl.orEmpty(),
|
value = folder.focusGifUrl.orEmpty(),
|
||||||
onValueChange = { CollectionEditorRepository.updateFolderFocusGifUrl(it) },
|
onValueChange = { CollectionEditorRepository.updateFolderFocusGifUrl(it) },
|
||||||
placeholder = "Always-play GIF URL (optional)",
|
placeholder = "Always-play GIF URL (optional)",
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
CollectionEditorRepository.updateFolderFocusGifEnabled(!folder.focusGifEnabled)
|
|
||||||
}
|
|
||||||
.padding(vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Show GIF When Configured",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
Switch(
|
|
||||||
checked = folder.focusGifEnabled,
|
|
||||||
onCheckedChange = { CollectionEditorRepository.updateFolderFocusGifEnabled(it) },
|
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tile Shape
|
NuvioSurfaceCard {
|
||||||
item {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Column {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Tile Shape",
|
text = "Tile Shape",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
FlowRow(
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
PosterShape.entries.forEach { shape ->
|
PosterShape.entries.forEach { shape ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = folder.posterShape == shape,
|
selected = folder.posterShape == shape,
|
||||||
|
|
@ -645,45 +668,29 @@ private fun FolderEditorSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Hide Title
|
FolderEditorToggleRow(
|
||||||
item {
|
title = "Show GIF When Configured",
|
||||||
Row(
|
subtitle = "Play the configured GIF instead of the static cover when available.",
|
||||||
modifier = Modifier
|
checked = folder.focusGifEnabled,
|
||||||
.fillMaxWidth()
|
onCheckedChange = { CollectionEditorRepository.updateFolderFocusGifEnabled(it) },
|
||||||
.clickable { CollectionEditorRepository.updateFolderHideTitle(!folder.hideTitle) }
|
|
||||||
.padding(vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Hide Title",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
)
|
||||||
Switch(
|
|
||||||
|
FolderEditorToggleRow(
|
||||||
|
title = "Hide Title",
|
||||||
|
subtitle = "Only show the artwork or emoji for this folder tile.",
|
||||||
checked = folder.hideTitle,
|
checked = folder.hideTitle,
|
||||||
onCheckedChange = { CollectionEditorRepository.updateFolderHideTitle(it) },
|
onCheckedChange = { CollectionEditorRepository.updateFolderHideTitle(it) },
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
|
||||||
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Catalog Sources
|
|
||||||
item {
|
item {
|
||||||
Row(
|
FolderEditorSection(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
title = "CATALOG SOURCES",
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
actions = {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
NuvioSectionLabel(text = "CATALOG SOURCES")
|
|
||||||
TextButton(onClick = { CollectionEditorRepository.showCatalogPicker() }) {
|
TextButton(onClick = { CollectionEditorRepository.showCatalogPicker() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
|
|
@ -693,137 +700,65 @@ private fun FolderEditorSheet(
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("Add")
|
Text("Add")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(folder.catalogSources) { index, source ->
|
|
||||||
val matchingCatalog = state.availableCatalogs.find {
|
|
||||||
it.addonId == source.addonId && it.type == source.type && it.catalogId == source.catalogId
|
|
||||||
}
|
|
||||||
val genreOptions = matchingCatalog?.genreOptions.orEmpty()
|
|
||||||
NuvioSurfaceCard {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = "${source.catalogId} (${source.type})",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = buildString {
|
|
||||||
append(source.addonId)
|
|
||||||
if (source.genre != null) append(" · ${source.genre}")
|
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = { CollectionEditorRepository.removeCatalogSource(index) },
|
|
||||||
modifier = Modifier.size(36.dp),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
if (folder.catalogSources.isEmpty()) {
|
||||||
imageVector = Icons.Rounded.Close,
|
NuvioSurfaceCard {
|
||||||
contentDescription = "Remove",
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (genreOptions.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = "Genre",
|
text = "No catalog sources yet",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
FilterChip(
|
|
||||||
selected = source.genre == null,
|
|
||||||
onClick = { CollectionEditorRepository.updateCatalogSourceGenre(index, null) },
|
|
||||||
label = { Text("All") },
|
|
||||||
leadingIcon = if (source.genre == null) {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else null,
|
|
||||||
)
|
|
||||||
genreOptions.forEach { genre ->
|
|
||||||
FilterChip(
|
|
||||||
selected = source.genre == genre,
|
|
||||||
onClick = { CollectionEditorRepository.updateCatalogSourceGenre(index, genre) },
|
|
||||||
label = { Text(genre) },
|
|
||||||
leadingIcon = if (source.genre == genre) {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folder.catalogSources.isEmpty()) {
|
|
||||||
item {
|
|
||||||
Text(
|
Text(
|
||||||
text = "No catalog sources. Tap \"Add\" to select from installed addons.",
|
text = "Add catalogs from your installed addons to define what this folder shows.",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(vertical = 8.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
folder.catalogSources.forEachIndexed { index, source ->
|
||||||
|
FolderCatalogSourceCard(
|
||||||
|
source = source,
|
||||||
|
matchingCatalog = state.availableCatalogs.find {
|
||||||
|
it.addonId == source.addonId && it.type == source.type && it.catalogId == source.catalogId
|
||||||
|
},
|
||||||
|
onRemove = { CollectionEditorRepository.removeCatalogSource(index) },
|
||||||
|
onOpenGenrePicker = { CollectionEditorRepository.showGenrePicker(index) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save / Cancel
|
|
||||||
item {
|
item {
|
||||||
Column(
|
Spacer(modifier = Modifier.height(96.dp + bottomInset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.background.copy(alpha = 0.96f),
|
||||||
|
tonalElevation = 6.dp,
|
||||||
|
shadowElevation = 10.dp,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 8.dp, bottom = 24.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
.padding(bottom = bottomInset),
|
||||||
) {
|
) {
|
||||||
NuvioPrimaryButton(
|
NuvioPrimaryButton(
|
||||||
text = "Save Folder",
|
text = "Save Folder",
|
||||||
enabled = folder.title.isNotBlank(),
|
enabled = folder.title.isNotBlank(),
|
||||||
onClick = { CollectionEditorRepository.saveFolderEdit() },
|
onClick = { CollectionEditorRepository.saveFolderEdit() },
|
||||||
)
|
)
|
||||||
androidx.compose.material3.Button(
|
|
||||||
onClick = { CollectionEditorRepository.cancelFolderEdit() },
|
|
||||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Cancel",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -839,7 +774,7 @@ private fun CatalogPickerSheet(
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
ModalBottomSheet(
|
NuvioModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
|
@ -866,6 +801,12 @@ private fun CatalogPickerSheet(
|
||||||
Text("Done")
|
Text("Done")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
text = "Choose the addon catalogs this folder should aggregate.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val grouped = availableCatalogs.groupBy { it.addonName }
|
val grouped = availableCatalogs.groupBy { it.addonName }
|
||||||
|
|
@ -936,3 +877,265 @@ private fun CatalogPickerSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun GenrePickerSheet(
|
||||||
|
title: String,
|
||||||
|
selectedGenre: String?,
|
||||||
|
genreOptions: List<String>,
|
||||||
|
allowAll: Boolean,
|
||||||
|
onSelect: (String?) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
NuvioModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Genre Filter",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowAll) {
|
||||||
|
item {
|
||||||
|
GenrePickerOptionRow(
|
||||||
|
title = "All genres",
|
||||||
|
selected = selectedGenre == null,
|
||||||
|
onClick = { onSelect(null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(genreOptions) { _, genre ->
|
||||||
|
GenrePickerOptionRow(
|
||||||
|
title = genre,
|
||||||
|
selected = selectedGenre == genre,
|
||||||
|
onClick = { onSelect(genre) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FolderEditorSection(
|
||||||
|
title: String,
|
||||||
|
actions: @Composable (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
NuvioSectionLabel(text = title)
|
||||||
|
actions?.invoke()
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FolderEditorToggleRow(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!checked) },
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun FolderCatalogSourceCard(
|
||||||
|
source: CollectionCatalogSource,
|
||||||
|
matchingCatalog: AvailableCatalog?,
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
onOpenGenrePicker: () -> Unit,
|
||||||
|
) {
|
||||||
|
val typeLabel = source.type.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase() else it.toString()
|
||||||
|
}
|
||||||
|
val metaLine = buildString {
|
||||||
|
append(typeLabel)
|
||||||
|
append(" · ${source.catalogId}")
|
||||||
|
}
|
||||||
|
val genreOptions = matchingCatalog?.genreOptions.orEmpty()
|
||||||
|
val selectedGenreLabel = source.genre ?: if (matchingCatalog?.genreRequired == true) "Select genre" else "All genres"
|
||||||
|
|
||||||
|
NuvioSurfaceCard {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = matchingCatalog?.catalogName ?: source.catalogId,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = matchingCatalog?.addonName ?: source.addonId,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = onRemove,
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = "Remove",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = metaLine,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (genreOptions.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onOpenGenrePicker),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Genre Filter",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = selectedGenreLabel,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(onClick = onOpenGenrePicker) {
|
||||||
|
Text("Choose")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GenrePickerOptionRow(
|
||||||
|
title: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val bgColor = if (selected) {
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
val borderColor = if (selected) {
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(bgColor)
|
||||||
|
.border(1.dp, borderColor, RoundedCornerShape(10.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 14.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue