diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index 712146318..066aa2029 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -5,12 +5,14 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.nuvio.app.features.addons.AddonStorage +import com.nuvio.app.features.home.HomeCatalogSettingsStorage class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) AddonStorage.initialize(applicationContext) + HomeCatalogSettingsStorage.initialize(applicationContext) setContent { App() diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.android.kt new file mode 100644 index 000000000..0d69b0abf --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.android.kt @@ -0,0 +1,25 @@ +package com.nuvio.app.features.home + +import android.content.Context +import android.content.SharedPreferences + +actual object HomeCatalogSettingsStorage { + private const val preferencesName = "nuvio_home_catalog_settings" + private const val payloadKey = "catalog_settings_payload" + + private var preferences: SharedPreferences? = null + + fun initialize(context: Context) { + preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE) + } + + actual fun loadPayload(): String? = + preferences?.getString(payloadKey, null) + + actual fun savePayload(payload: String) { + preferences + ?.edit() + ?.putString(payloadKey, payload) + ?.apply() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogDefinitions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogDefinitions.kt new file mode 100644 index 000000000..5184318fc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogDefinitions.kt @@ -0,0 +1,39 @@ +package com.nuvio.app.features.home + +import com.nuvio.app.features.addons.ManagedAddon +import com.nuvio.app.features.catalog.supportsPagination + +data class HomeCatalogDefinition( + val key: String, + val defaultTitle: String, + val addonName: String, + val manifestUrl: String, + val type: String, + val catalogId: String, + val supportsPagination: Boolean, +) + +fun buildHomeCatalogDefinitions(addons: List): List = + addons.mapNotNull { addon -> + val manifest = addon.manifest ?: return@mapNotNull null + addon to manifest + }.flatMap { (addon, manifest) -> + manifest.catalogs + .filter { catalog -> catalog.extra.none { it.isRequired } } + .map { catalog -> + HomeCatalogDefinition( + key = "${manifest.id}:${catalog.type}:${catalog.id}", + defaultTitle = "${catalog.name} - ${catalog.type.displayLabel()}", + addonName = manifest.name, + manifestUrl = addon.manifestUrl, + type = catalog.type, + catalogId = catalog.id, + supportsPagination = catalog.supportsPagination(), + ) + } + } + +internal fun String.displayLabel(): String = + replaceFirstChar { char -> + if (char.isLowerCase()) char.titlecase() else char.toString() + } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsRepository.kt new file mode 100644 index 000000000..c56a5a343 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsRepository.kt @@ -0,0 +1,209 @@ +package com.nuvio.app.features.home + +import com.nuvio.app.features.addons.ManagedAddon +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +data class HomeCatalogSettingsItem( + val key: String, + val defaultTitle: String, + val addonName: String, + val customTitle: String = "", + val enabled: Boolean = true, + val order: Int = 0, +) { + val displayTitle: String + get() = customTitle.ifBlank { defaultTitle } +} + +data class HomeCatalogSettingsUiState( + val items: List = emptyList(), +) { + val signature: String + get() = items.joinToString(separator = "|") { item -> + "${item.key}:${item.order}:${item.enabled}:${item.customTitle}" + } +} + +internal data class HomeCatalogPreference( + val customTitle: String, + val enabled: Boolean, + val order: Int, +) + +@Serializable +private data class StoredHomeCatalogPreference( + val key: String, + val customTitle: String = "", + val enabled: Boolean = true, + val order: Int = 0, +) + +object HomeCatalogSettingsRepository { + private val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + + private val _uiState = MutableStateFlow(HomeCatalogSettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var hasLoaded = false + private var definitions: List = emptyList() + private var preferences: MutableMap = mutableMapOf() + + fun syncCatalogs(addons: List) { + ensureLoaded() + definitions = buildHomeCatalogDefinitions(addons) + normalizePreferences() + publish() + persist() + } + + internal fun snapshot(): Map { + ensureLoaded() + return preferences.mapValues { (_, value) -> + HomeCatalogPreference( + customTitle = value.customTitle, + enabled = value.enabled, + order = value.order, + ) + } + } + + fun setEnabled(key: String, enabled: Boolean) { + updatePreference(key) { preference -> + preference.copy(enabled = enabled) + } + } + + fun setCustomTitle(key: String, title: String) { + updatePreference(key) { preference -> + preference.copy(customTitle = title) + } + } + + fun moveUp(key: String) { + move(key = key, direction = -1) + } + + fun moveDown(key: String) { + move(key = key, direction = 1) + } + + private fun ensureLoaded() { + if (hasLoaded) return + hasLoaded = true + + val payload = HomeCatalogSettingsStorage.loadPayload().orEmpty().trim() + if (payload.isEmpty()) return + + val stored = runCatching { + json.decodeFromString>(payload) + }.getOrDefault(emptyList()) + + preferences = stored.associateBy { it.key }.toMutableMap() + } + + private fun normalizePreferences() { + val current = preferences + val orderedDefinitions = definitions.mapIndexed { defaultIndex, definition -> + Triple( + definition, + current[definition.key]?.order ?: defaultIndex, + defaultIndex, + ) + }.sortedWith( + compareBy>( + { it.second }, + { it.third }, + ), + ).map { it.first } + + val normalized = mutableMapOf() + orderedDefinitions.forEachIndexed { index, definition -> + val stored = current[definition.key] + normalized[definition.key] = StoredHomeCatalogPreference( + key = definition.key, + customTitle = stored?.customTitle.orEmpty(), + enabled = stored?.enabled ?: true, + order = index, + ) + } + preferences = normalized + } + + private fun publish() { + val items = definitions + .sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE } + .map { definition -> + val preference = preferences[definition.key] + HomeCatalogSettingsItem( + key = definition.key, + defaultTitle = definition.defaultTitle, + addonName = definition.addonName, + customTitle = preference?.customTitle.orEmpty(), + enabled = preference?.enabled ?: true, + order = preference?.order ?: 0, + ) + } + + _uiState.value = HomeCatalogSettingsUiState(items = items) + } + + private fun persist() { + HomeCatalogSettingsStorage.savePayload( + json.encodeToString( + preferences.values.sortedBy { it.order }, + ), + ) + } + + private fun updatePreference( + key: String, + transform: (StoredHomeCatalogPreference) -> StoredHomeCatalogPreference, + ) { + ensureLoaded() + val current = preferences[key] ?: return + preferences[key] = transform(current) + publish() + persist() + HomeRepository.applyCurrentSettings() + } + + private fun move( + key: String, + direction: Int, + ) { + ensureLoaded() + if (definitions.isEmpty()) return + + val orderedKeys = definitions + .sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE } + .map { it.key } + .toMutableList() + + val currentIndex = orderedKeys.indexOf(key) + if (currentIndex == -1) return + + val targetIndex = currentIndex + direction + if (targetIndex !in orderedKeys.indices) return + + val movingKey = orderedKeys.removeAt(currentIndex) + orderedKeys.add(targetIndex, movingKey) + + orderedKeys.forEachIndexed { index, itemKey -> + val current = preferences[itemKey] ?: return@forEachIndexed + preferences[itemKey] = current.copy(order = index) + } + + publish() + persist() + HomeRepository.applyCurrentSettings() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.kt new file mode 100644 index 000000000..d6452b880 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.kt @@ -0,0 +1,6 @@ +package com.nuvio.app.features.home + +internal expect object HomeCatalogSettingsStorage { + fun loadPayload(): String? + fun savePayload(payload: String) +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeRepository.kt index 89deb1c05..c7c6212cb 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeRepository.kt @@ -2,7 +2,6 @@ package com.nuvio.app.features.home import com.nuvio.app.features.addons.ManagedAddon import com.nuvio.app.features.catalog.fetchCatalogPage -import com.nuvio.app.features.catalog.supportsPagination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -20,17 +19,26 @@ object HomeRepository { val uiState: StateFlow = _uiState.asStateFlow() private var lastRequestKey: String? = null + private var currentDefinitions: List = emptyList() + private var cachedSections: Map = emptyMap() + private var lastErrorMessage: String? = null fun refresh(addons: List, force: Boolean = false) { - val requests = buildCatalogRequests(addons) + val requests = buildHomeCatalogDefinitions(addons) + currentDefinitions = requests val requestKey = requests.joinToString(separator = "|") { request -> - "${request.addon.manifestUrl}:${request.type}:${request.catalogId}" + "${request.manifestUrl}:${request.type}:${request.catalogId}" } - if (!force && requestKey == lastRequestKey) return + if (!force && requestKey == lastRequestKey && cachedSections.isNotEmpty()) { + applyCurrentSettings() + return + } lastRequestKey = requestKey if (requests.isEmpty()) { + cachedSections = emptyMap() + lastErrorMessage = null _uiState.value = HomeUiState( isLoading = false, sections = emptyList(), @@ -47,60 +55,55 @@ object HomeRepository { } }.awaitAll() - val sections = results.mapNotNull { it.getOrNull() } - val firstFailure = results.firstNotNullOfOrNull { it.exceptionOrNull()?.message } - - _uiState.value = HomeUiState( - isLoading = false, - sections = sections, - errorMessage = if (sections.isEmpty()) firstFailure else null, - ) + cachedSections = results + .mapNotNull { it.getOrNull() } + .associateBy { it.key } + lastErrorMessage = results.firstNotNullOfOrNull { it.exceptionOrNull()?.message } + applyCurrentSettings() } } - private fun buildCatalogRequests(addons: List): List = - addons.mapNotNull { addon -> - val manifest = addon.manifest ?: return@mapNotNull null - addon to manifest - }.flatMap { (addon, manifest) -> - manifest.catalogs - .filter { catalog -> catalog.extra.none { it.isRequired } } - .map { catalog -> - CatalogRequest( - addon = addon, - catalogId = catalog.id, - catalogName = catalog.name, - type = catalog.type, - supportsPagination = catalog.supportsPagination(), - ) - } - } + fun applyCurrentSettings() { + val preferences = HomeCatalogSettingsRepository.snapshot() + val sections = currentDefinitions + .sortedBy { definition -> preferences[definition.key]?.order ?: Int.MAX_VALUE } + .mapNotNull { definition -> + val preference = preferences[definition.key] + if (preference?.enabled == false) return@mapNotNull null - private suspend fun CatalogRequest.toSection(): HomeCatalogSection { - val manifest = requireNotNull(addon.manifest) + val section = cachedSections[definition.key] ?: return@mapNotNull null + val customTitle = preference?.customTitle.orEmpty() + section.copy( + title = customTitle.ifBlank { section.title }, + ) + } + + _uiState.value = HomeUiState( + isLoading = false, + sections = sections, + errorMessage = if (sections.isEmpty()) lastErrorMessage else null, + ) + } + + private suspend fun HomeCatalogDefinition.toSection(): HomeCatalogSection { val page = fetchCatalogPage( - manifestUrl = manifest.transportUrl, + manifestUrl = manifestUrl, type = type, catalogId = catalogId, ) val items = page.items - require(items.isNotEmpty()) { "No feed items returned for $catalogName." } + require(items.isNotEmpty()) { "No feed items returned for $defaultTitle." } return HomeCatalogSection( - key = "${manifest.id}:$type:$catalogId", - title = "$catalogName - ${type.displayLabel()}", - subtitle = manifest.name, - addonName = manifest.name, + key = key, + title = defaultTitle, + subtitle = addonName, + addonName = addonName, type = type, - manifestUrl = manifest.transportUrl, + manifestUrl = manifestUrl, catalogId = catalogId, items = items, supportsPagination = supportsPagination, ) } } - -private fun String.displayLabel(): String = - replaceFirstChar { char -> - if (char.isLowerCase()) char.titlecase() else char.toString() - } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index 3445a9a7a..8e8e54dff 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -41,6 +41,7 @@ fun HomeScreen( } LaunchedEffect(catalogRefreshKey) { + HomeCatalogSettingsRepository.syncCatalogs(addonsUiState.addons) HomeRepository.refresh(addonsUiState.addons) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt index 80c8d4ca7..9256c18e5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt @@ -21,18 +21,27 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.rounded.Extension +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.Tune import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,11 +53,17 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import com.nuvio.app.core.ui.NuvioScreen import com.nuvio.app.core.ui.NuvioScreenHeader import com.nuvio.app.core.ui.NuvioSectionLabel +import com.nuvio.app.features.addons.AddonRepository +import com.nuvio.app.features.home.HomeCatalogSettingsItem +import com.nuvio.app.features.home.HomeCatalogSettingsRepository +import com.nuvio.app.features.home.components.HomeEmptyStateCard +import androidx.lifecycle.compose.collectAsStateWithLifecycle private enum class SettingsCategory( val label: String, @@ -62,6 +77,7 @@ private enum class SettingsPage( ) { Root("Settings"), ContentDiscovery("Content & Discovery"), + Homescreen("Homescreen"), } @Composable @@ -74,6 +90,17 @@ fun SettingsScreen( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { + LaunchedEffect(Unit) { + AddonRepository.initialize() + } + + val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle() + val homescreenSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(addonsUiState.addons) { + HomeCatalogSettingsRepository.syncCatalogs(addonsUiState.addons) + } + var currentPage by rememberSaveable { mutableStateOf(SettingsPage.Root.name) } val page = remember(currentPage) { SettingsPage.valueOf(currentPage) } @@ -82,12 +109,14 @@ fun SettingsScreen( page = page, onPageChange = { currentPage = it.name }, onAddonsClick = onAddonsClick, + homescreenSettings = homescreenSettingsUiState.items, ) } else { MobileSettingsScreen( page = page, onPageChange = { currentPage = it.name }, onAddonsClick = onAddonsClick, + homescreenSettings = homescreenSettingsUiState.items, ) } } @@ -98,6 +127,7 @@ private fun MobileSettingsScreen( page: SettingsPage, onPageChange: (SettingsPage) -> Unit, onAddonsClick: () -> Unit, + homescreenSettings: List, ) { NuvioScreen { stickyHeader { @@ -119,6 +149,11 @@ private fun MobileSettingsScreen( SettingsPage.ContentDiscovery -> contentDiscoveryContent( isTablet = false, onAddonsClick = onAddonsClick, + onHomescreenClick = { onPageChange(SettingsPage.Homescreen) }, + ) + SettingsPage.Homescreen -> homescreenSettingsContent( + isTablet = false, + items = homescreenSettings, ) } } @@ -129,6 +164,7 @@ private fun TabletSettingsScreen( page: SettingsPage, onPageChange: (SettingsPage) -> Unit, onAddonsClick: () -> Unit, + homescreenSettings: List, ) { var selectedCategory by rememberSaveable { mutableStateOf(SettingsCategory.General.name) } val activeCategory = SettingsCategory.valueOf(selectedCategory) @@ -195,13 +231,18 @@ private fun TabletSettingsScreen( SettingsPage.ContentDiscovery -> contentDiscoveryContent( isTablet = true, onAddonsClick = onAddonsClick, + onHomescreenClick = { onPageChange(SettingsPage.Homescreen) }, + ) + SettingsPage.Homescreen -> homescreenSettingsContent( + isTablet = true, + items = homescreenSettings, ) } } } } -private fun androidx.compose.foundation.lazy.LazyListScope.settingsRootContent( +private fun LazyListScope.settingsRootContent( isTablet: Boolean, onContentDiscoveryClick: () -> Unit, ) { @@ -221,13 +262,14 @@ private fun androidx.compose.foundation.lazy.LazyListScope.settingsRootContent( } } -private fun androidx.compose.foundation.lazy.LazyListScope.contentDiscoveryContent( +private fun LazyListScope.contentDiscoveryContent( isTablet: Boolean, onAddonsClick: () -> Unit, + onHomescreenClick: () -> Unit, ) { item { SettingsSection( - title = "GENERAL", + title = "SOURCES", isTablet = isTablet, ) { SettingsNavigationRow( @@ -239,6 +281,56 @@ private fun androidx.compose.foundation.lazy.LazyListScope.contentDiscoveryConte ) } } + item { + SettingsSection( + title = "HOME", + isTablet = isTablet, + ) { + SettingsNavigationRow( + title = "Homescreen", + description = "Control which catalogs appear on Home and in what order.", + icon = Icons.Rounded.Tune, + isTablet = isTablet, + onClick = onHomescreenClick, + ) + } + } +} + +private fun LazyListScope.homescreenSettingsContent( + isTablet: Boolean, + items: List, +) { + item { + if (items.isEmpty()) { + HomeEmptyStateCard( + modifier = Modifier.fillMaxWidth(), + title = "No home catalogs", + message = "Install an addon with board-compatible catalogs to configure Homescreen rows.", + ) + } else { + SettingsSection( + title = "CATALOGS", + isTablet = isTablet, + ) { + items.forEachIndexed { index, item -> + HomescreenCatalogRow( + item = item, + isTablet = isTablet, + canMoveUp = index > 0, + canMoveDown = index < items.lastIndex, + onTitleChange = { HomeCatalogSettingsRepository.setCustomTitle(item.key, it) }, + onEnabledChange = { HomeCatalogSettingsRepository.setEnabled(item.key, it) }, + onMoveUp = { HomeCatalogSettingsRepository.moveUp(item.key) }, + onMoveDown = { HomeCatalogSettingsRepository.moveDown(item.key) }, + ) + if (index < items.lastIndex) { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + } + } + } + } + } } @Composable @@ -418,3 +510,132 @@ private fun SettingsNavigationRow( ) } } + +@Composable +private fun HomescreenCatalogRow( + item: HomeCatalogSettingsItem, + isTablet: Boolean, + canMoveUp: Boolean, + canMoveDown: Boolean, + onTitleChange: (String) -> Unit, + onEnabledChange: (Boolean) -> Unit, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, +) { + val horizontalPadding = if (isTablet) 20.dp else 16.dp + val verticalPadding = if (isTablet) 18.dp else 16.dp + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = horizontalPadding, vertical = verticalPadding), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column( + modifier = Modifier + .padding(end = 12.dp) + .widthIn(max = if (isTablet) 560.dp else 260.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = item.displayTitle, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = item.addonName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch( + checked = item.enabled, + onCheckedChange = onEnabledChange, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.onPrimary, + checkedTrackColor = MaterialTheme.colorScheme.primary, + uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant, + uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant, + ), + ) + } + + OutlinedTextField( + value = item.customTitle, + onValueChange = onTitleChange, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { + Text("Display Name") + }, + placeholder = { + Text(item.defaultTitle) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + ), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MoveActionChip( + label = "Move Up", + icon = Icons.Rounded.KeyboardArrowUp, + enabled = canMoveUp, + onClick = onMoveUp, + ) + MoveActionChip( + label = "Move Down", + icon = Icons.Rounded.KeyboardArrowDown, + enabled = canMoveDown, + onClick = onMoveDown, + ) + } + } +} + +@Composable +private fun MoveActionChip( + label: String, + icon: ImageVector, + enabled: Boolean, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .clickable(enabled = enabled, onClick = onClick) + .alpha(if (enabled) 1f else 0.45f), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.10f), + shape = RoundedCornerShape(999.dp), + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium, + ) + } + } +} diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.ios.kt new file mode 100644 index 000000000..41a4b8ad6 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsStorage.ios.kt @@ -0,0 +1,14 @@ +package com.nuvio.app.features.home + +import platform.Foundation.NSUserDefaults + +actual object HomeCatalogSettingsStorage { + private const val payloadKey = "catalog_settings_payload" + + actual fun loadPayload(): String? = + NSUserDefaults.standardUserDefaults.stringForKey(payloadKey) + + actual fun savePayload(payload: String) { + NSUserDefaults.standardUserDefaults.setObject(payload, forKey = payloadKey) + } +}