mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: implement home catalog settings management with storage and UI integration
This commit is contained in:
parent
3c4cc6b59f
commit
1c2319f2ee
9 changed files with 566 additions and 46 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ManagedAddon>): List<HomeCatalogDefinition> =
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<HomeCatalogSettingsItem> = 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<HomeCatalogSettingsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var hasLoaded = false
|
||||
private var definitions: List<HomeCatalogDefinition> = emptyList()
|
||||
private var preferences: MutableMap<String, StoredHomeCatalogPreference> = mutableMapOf()
|
||||
|
||||
fun syncCatalogs(addons: List<ManagedAddon>) {
|
||||
ensureLoaded()
|
||||
definitions = buildHomeCatalogDefinitions(addons)
|
||||
normalizePreferences()
|
||||
publish()
|
||||
persist()
|
||||
}
|
||||
|
||||
internal fun snapshot(): Map<String, HomeCatalogPreference> {
|
||||
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<List<StoredHomeCatalogPreference>>(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<Triple<HomeCatalogDefinition, Int, Int>>(
|
||||
{ it.second },
|
||||
{ it.third },
|
||||
),
|
||||
).map { it.first }
|
||||
|
||||
val normalized = mutableMapOf<String, StoredHomeCatalogPreference>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.home
|
||||
|
||||
internal expect object HomeCatalogSettingsStorage {
|
||||
fun loadPayload(): String?
|
||||
fun savePayload(payload: String)
|
||||
}
|
||||
|
|
@ -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<HomeUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var lastRequestKey: String? = null
|
||||
private var currentDefinitions: List<HomeCatalogDefinition> = emptyList()
|
||||
private var cachedSections: Map<String, HomeCatalogSection> = emptyMap()
|
||||
private var lastErrorMessage: String? = null
|
||||
|
||||
fun refresh(addons: List<ManagedAddon>, 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<ManagedAddon>): List<CatalogRequest> =
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ fun HomeScreen(
|
|||
}
|
||||
|
||||
LaunchedEffect(catalogRefreshKey) {
|
||||
HomeCatalogSettingsRepository.syncCatalogs(addonsUiState.addons)
|
||||
HomeRepository.refresh(addonsUiState.addons)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HomeCatalogSettingsItem>,
|
||||
) {
|
||||
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<HomeCatalogSettingsItem>,
|
||||
) {
|
||||
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<HomeCatalogSettingsItem>,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue