mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +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 showFolderEditor: Boolean = false,
|
||||
val showCatalogPicker: Boolean = false,
|
||||
val genrePickerSourceIndex: Int? = null,
|
||||
)
|
||||
|
||||
object CollectionEditorRepository {
|
||||
|
|
@ -224,6 +225,7 @@ object CollectionEditorRepository {
|
|||
editingFolder = folder.copy(
|
||||
catalogSources = folder.catalogSources.toMutableList().apply { removeAt(index) },
|
||||
),
|
||||
genrePickerSourceIndex = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -250,13 +252,29 @@ object CollectionEditorRepository {
|
|||
}
|
||||
|
||||
fun showCatalogPicker() {
|
||||
_uiState.value = _uiState.value.copy(showCatalogPicker = true)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
showCatalogPicker = true,
|
||||
genrePickerSourceIndex = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun hideCatalogPicker() {
|
||||
_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() {
|
||||
val folder = _uiState.value.editingFolder ?: return
|
||||
val existing = _uiState.value.folders
|
||||
|
|
@ -270,6 +288,7 @@ object CollectionEditorRepository {
|
|||
editingFolder = null,
|
||||
showFolderEditor = false,
|
||||
showCatalogPicker = false,
|
||||
genrePickerSourceIndex = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -278,6 +297,7 @@ object CollectionEditorRepository {
|
|||
editingFolder = null,
|
||||
showFolderEditor = 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.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -36,7 +35,6 @@ import androidx.compose.material3.FilterChip
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
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.animation.core.animateDpAsState
|
||||
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.NuvioScreen
|
||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
|
|
@ -81,11 +80,49 @@ fun CollectionEditorScreen(
|
|||
CollectionEditorRepository.initialize(collectionId)
|
||||
}
|
||||
|
||||
if (state.showFolderEditor && state.editingFolder != null) {
|
||||
FolderEditorSheet(
|
||||
val editingFolder = state.editingFolder
|
||||
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,
|
||||
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) {
|
||||
|
|
@ -494,290 +531,203 @@ private fun FolderListItem(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun FolderEditorSheet(
|
||||
private fun FolderEditorPage(
|
||||
state: CollectionEditorUiState,
|
||||
onDismiss: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val folder = state.editingFolder ?: return
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val bottomInset = nuvioPlatformExtraBottomPadding
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
LazyColumn(
|
||||
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,
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
NuvioScreen(modifier = Modifier.fillMaxSize()) {
|
||||
stickyHeader {
|
||||
NuvioScreenHeader(
|
||||
title = if (state.folders.any { it.id == folder.id }) "Edit Folder" else "New Folder",
|
||||
onBack = onBack,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
NuvioInputField(
|
||||
value = folder.title,
|
||||
onValueChange = { CollectionEditorRepository.updateFolderTitle(it) },
|
||||
placeholder = "Folder Title",
|
||||
)
|
||||
}
|
||||
|
||||
// Cover (emoji or image url)
|
||||
item {
|
||||
Column {
|
||||
Text(
|
||||
text = "Cover",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FilterChip(
|
||||
selected = folder.coverEmoji == null && folder.coverImageUrl == null,
|
||||
onClick = { CollectionEditorRepository.clearFolderCover() },
|
||||
label = { Text("None") },
|
||||
)
|
||||
FilterChip(
|
||||
selected = folder.coverEmoji != null,
|
||||
onClick = {
|
||||
if (folder.coverEmoji == null) {
|
||||
CollectionEditorRepository.updateFolderCoverEmoji("📁")
|
||||
}
|
||||
},
|
||||
label = { Text("Emoji") },
|
||||
)
|
||||
FilterChip(
|
||||
selected = folder.coverImageUrl != null,
|
||||
onClick = {
|
||||
if (folder.coverImageUrl == null) {
|
||||
CollectionEditorRepository.updateFolderCoverImage("")
|
||||
}
|
||||
},
|
||||
label = { Text("Image") },
|
||||
)
|
||||
}
|
||||
if (folder.coverEmoji != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
NuvioInputField(
|
||||
value = folder.coverEmoji,
|
||||
onValueChange = { CollectionEditorRepository.updateFolderCoverEmoji(it) },
|
||||
placeholder = "Emoji",
|
||||
modifier = Modifier.width(100.dp),
|
||||
)
|
||||
}
|
||||
if (folder.coverImageUrl != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
NuvioInputField(
|
||||
value = folder.coverImageUrl,
|
||||
onValueChange = { CollectionEditorRepository.updateFolderCoverImage(it) },
|
||||
placeholder = "Image URL",
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
NuvioInputField(
|
||||
value = folder.focusGifUrl.orEmpty(),
|
||||
onValueChange = { CollectionEditorRepository.updateFolderFocusGifUrl(it) },
|
||||
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
|
||||
item {
|
||||
Column {
|
||||
Text(
|
||||
text = "Tile Shape",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PosterShape.entries.forEach { shape ->
|
||||
FilterChip(
|
||||
selected = folder.posterShape == shape,
|
||||
onClick = { CollectionEditorRepository.updateFolderTileShape(shape) },
|
||||
label = { Text(shape.name) },
|
||||
leadingIcon = if (folder.posterShape == shape) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide Title
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.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(
|
||||
checked = folder.hideTitle,
|
||||
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 {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NuvioSectionLabel(text = "CATALOG SOURCES")
|
||||
TextButton(onClick = { CollectionEditorRepository.showCatalogPicker() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
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(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = "Remove",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
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(
|
||||
value = folder.title,
|
||||
onValueChange = { CollectionEditorRepository.updateFolderTitle(it) },
|
||||
placeholder = "Folder Title",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
FolderEditorSection(title = "APPEARANCE") {
|
||||
NuvioSurfaceCard {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = "Cover",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
selected = folder.coverEmoji == null && folder.coverImageUrl == null,
|
||||
onClick = { CollectionEditorRepository.clearFolderCover() },
|
||||
label = { Text("None") },
|
||||
)
|
||||
FilterChip(
|
||||
selected = folder.coverEmoji != null,
|
||||
onClick = {
|
||||
if (folder.coverEmoji == null) {
|
||||
CollectionEditorRepository.updateFolderCoverEmoji("📁")
|
||||
}
|
||||
},
|
||||
label = { Text("Emoji") },
|
||||
)
|
||||
FilterChip(
|
||||
selected = folder.coverImageUrl != null,
|
||||
onClick = {
|
||||
if (folder.coverImageUrl == null) {
|
||||
CollectionEditorRepository.updateFolderCoverImage("")
|
||||
}
|
||||
},
|
||||
label = { Text("Image") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (folder.coverEmoji != null) {
|
||||
NuvioInputField(
|
||||
value = folder.coverEmoji,
|
||||
onValueChange = { CollectionEditorRepository.updateFolderCoverEmoji(it) },
|
||||
placeholder = "Emoji",
|
||||
modifier = Modifier.width(100.dp),
|
||||
)
|
||||
}
|
||||
|
||||
if (folder.coverImageUrl != null) {
|
||||
NuvioInputField(
|
||||
value = folder.coverImageUrl,
|
||||
onValueChange = { CollectionEditorRepository.updateFolderCoverImage(it) },
|
||||
placeholder = "Image URL",
|
||||
)
|
||||
}
|
||||
|
||||
NuvioInputField(
|
||||
value = folder.focusGifUrl.orEmpty(),
|
||||
onValueChange = { CollectionEditorRepository.updateFolderFocusGifUrl(it) },
|
||||
placeholder = "Always-play GIF URL (optional)",
|
||||
)
|
||||
}
|
||||
}
|
||||
if (genreOptions.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Genre",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
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),
|
||||
|
||||
NuvioSurfaceCard {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = "Tile Shape",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
PosterShape.entries.forEach { shape ->
|
||||
FilterChip(
|
||||
selected = folder.posterShape == shape,
|
||||
onClick = { CollectionEditorRepository.updateFolderTileShape(shape) },
|
||||
label = { Text(shape.name) },
|
||||
leadingIcon = if (folder.posterShape == shape) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
}
|
||||
}
|
||||
|
||||
FolderEditorToggleRow(
|
||||
title = "Show GIF When Configured",
|
||||
subtitle = "Play the configured GIF instead of the static cover when available.",
|
||||
checked = folder.focusGifEnabled,
|
||||
onCheckedChange = { CollectionEditorRepository.updateFolderFocusGifEnabled(it) },
|
||||
)
|
||||
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,
|
||||
|
||||
FolderEditorToggleRow(
|
||||
title = "Hide Title",
|
||||
subtitle = "Only show the artwork or emoji for this folder tile.",
|
||||
checked = folder.hideTitle,
|
||||
onCheckedChange = { CollectionEditorRepository.updateFolderHideTitle(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
FolderEditorSection(
|
||||
title = "CATALOG SOURCES",
|
||||
actions = {
|
||||
TextButton(onClick = { CollectionEditorRepository.showCatalogPicker() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Add")
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (folder.catalogSources.isEmpty()) {
|
||||
NuvioSurfaceCard {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = "No catalog sources yet",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = "Add catalogs from your installed addons to define what this folder shows.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
} 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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -785,45 +735,30 @@ private fun FolderEditorSheet(
|
|||
}
|
||||
}
|
||||
|
||||
if (folder.catalogSources.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "No catalog sources. Tap \"Add\" to select from installed addons.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Save / Cancel
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
NuvioPrimaryButton(
|
||||
text = "Save Folder",
|
||||
enabled = folder.title.isNotBlank(),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.padding(bottom = bottomInset),
|
||||
) {
|
||||
NuvioPrimaryButton(
|
||||
text = "Save Folder",
|
||||
enabled = folder.title.isNotBlank(),
|
||||
onClick = { CollectionEditorRepository.saveFolderEdit() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -839,7 +774,7 @@ private fun CatalogPickerSheet(
|
|||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
NuvioModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
|
|
@ -866,6 +801,12 @@ private fun CatalogPickerSheet(
|
|||
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 }
|
||||
|
|
@ -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