Save/load from server

This commit is contained in:
Damontecres 2026-01-29 15:49:25 -05:00
parent e11b9ecc72
commit f315531db7
No known key found for this signature in database
7 changed files with 192 additions and 33 deletions

View file

@ -399,7 +399,7 @@ class HomeSettingsService
/** /**
* Converts a [HomeRowConfig] into [HomeRowConfigDisplay] for UI purposes * Converts a [HomeRowConfig] into [HomeRowConfigDisplay] for UI purposes
*/ */
private suspend fun resolve( suspend fun resolve(
id: Int, id: Int,
config: HomeRowConfig, config: HomeRowConfig,
): HomeRowConfigDisplay = ): HomeRowConfigDisplay =

View file

@ -0,0 +1,56 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ListItem
import androidx.tv.material3.ListItemBorder
import androidx.tv.material3.ListItemColors
import androidx.tv.material3.ListItemDefaults
import androidx.tv.material3.ListItemGlow
import androidx.tv.material3.ListItemScale
import androidx.tv.material3.ListItemShape
@Composable
@NonRestartableComposable
fun HomeSettingsListItem(
selected: Boolean,
onClick: () -> Unit,
headlineContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
onLongClick: (() -> Unit)? = null,
overlineContent: (@Composable () -> Unit)? = null,
supportingContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
tonalElevation: Dp = 3.dp,
shape: ListItemShape = ListItemDefaults.shape(),
colors: ListItemColors = ListItemDefaults.colors(),
scale: ListItemScale = ListItemDefaults.scale(),
border: ListItemBorder = ListItemDefaults.border(),
glow: ListItemGlow = ListItemDefaults.glow(),
interactionSource: MutableInteractionSource? = null,
) = ListItem(
selected = selected,
onClick = onClick,
headlineContent = headlineContent,
modifier = modifier,
enabled = enabled,
onLongClick = onLongClick,
overlineContent = overlineContent,
supportingContent = supportingContent,
leadingContent = leadingContent,
trailingContent = trailingContent,
tonalElevation = tonalElevation,
shape = shape,
colors = colors,
scale = scale,
border = border,
glow = glow,
interactionSource = interactionSource,
)

View file

@ -12,8 +12,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@ -23,6 +27,8 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsDestination.ChooseRowType import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsDestination.ChooseRowType
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsDestination.RowSettings import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsDestination.RowSettings
@ -41,6 +47,7 @@ fun HomeSettingsPage(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val backStack = rememberNavBackStack(HomeSettingsDestination.RowList) val backStack = rememberNavBackStack(HomeSettingsDestination.RowList)
var showConfirmDialog by remember { mutableStateOf<(() -> Unit)?>(null) }
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val discoverEnabled by viewModel.discoverEnabled.collectAsState(false) val discoverEnabled by viewModel.discoverEnabled.collectAsState(false)
@ -86,7 +93,16 @@ fun HomeSettingsPage(
HomeSettingsRowList( HomeSettingsRowList(
state = state, state = state,
onClickAdd = { backStack.add(HomeSettingsDestination.AddRow) }, onClickAdd = { backStack.add(HomeSettingsDestination.AddRow) },
onClickSaveLocal = { viewModel.saveToLocal() }, onClickSave = {
showConfirmDialog = {
viewModel.saveToRemote()
}
},
onClickLoad = {
showConfirmDialog = {
viewModel.loadFromRemote()
}
},
onClickMove = viewModel::moveRow, onClickMove = viewModel::moveRow,
onClickDelete = viewModel::deleteRow, onClickDelete = viewModel::deleteRow,
onClick = { index, row -> onClick = { index, row ->
@ -189,4 +205,15 @@ fun HomeSettingsPage(
.weight(1f), .weight(1f),
) )
} }
showConfirmDialog?.let { onConfirm ->
ConfirmDialog(
title = stringResource(R.string.confirm),
body = "Overwrite?",
onCancel = { showConfirmDialog = null },
onConfirm = {
onConfirm.invoke()
showConfirmDialog = null
},
)
}
} }

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -27,9 +26,11 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.services.HomeRowConfigDisplay import com.github.damontecres.wholphin.services.HomeRowConfigDisplay
@ -47,7 +48,8 @@ fun HomeSettingsRowList(
state: HomePageSettingsState, state: HomePageSettingsState,
onClick: (Int, HomeRowConfigDisplay) -> Unit, onClick: (Int, HomeRowConfigDisplay) -> Unit,
onClickResize: (Int) -> Unit, onClickResize: (Int) -> Unit,
onClickSaveLocal: () -> Unit, onClickSave: () -> Unit,
onClickLoad: () -> Unit,
onClickAdd: () -> Unit, onClickAdd: () -> Unit,
onClickMove: (MoveDirection, Int) -> Unit, onClickMove: (MoveDirection, Int) -> Unit,
onClickDelete: (Int) -> Unit, onClickDelete: (Int) -> Unit,
@ -82,21 +84,57 @@ fun HomeSettingsRowList(
modifier = Modifier.focusRequester(firstFocus), modifier = Modifier.focusRequester(firstFocus),
) )
} }
item {
Row {
HomeSettingsListItem(
selected = false,
headlineContent = {
Text(
text = "Increase card sizes",
)
},
leadingContent = {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
},
onClick = { onClickResize.invoke(1) },
modifier = Modifier.weight(1f),
)
HomeSettingsListItem(
selected = false,
headlineContent = {
Text(
text = "Decrease card sizes",
)
},
leadingContent = {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
},
onClick = { onClickResize.invoke(-1) },
modifier = Modifier.weight(1f),
)
}
}
item { item {
HomeSettingsListItem( HomeSettingsListItem(
selected = false, selected = false,
headlineContent = { headlineContent = {
Text( Text(
text = "Increase card sizes", text = stringResource(R.string.save_to_server),
) )
}, },
leadingContent = { leadingContent = {
Icon( Text(
imageVector = Icons.Default.Edit, text = stringResource(R.string.fa_cloud_arrow_up),
contentDescription = null, fontFamily = FontAwesome,
) )
}, },
onClick = { onClickResize.invoke(1) }, onClick = onClickSave,
modifier = Modifier, modifier = Modifier,
) )
} }
@ -105,38 +143,26 @@ fun HomeSettingsRowList(
selected = false, selected = false,
headlineContent = { headlineContent = {
Text( Text(
text = "Decrease card sizes", text = stringResource(R.string.load_from_server),
) )
}, },
leadingContent = { leadingContent = {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
},
onClick = { onClickResize.invoke(-1) },
modifier = Modifier,
)
}
item {
HomeSettingsListItem(
selected = false,
headlineContent = {
Text( Text(
text = stringResource(R.string.save), text = stringResource(R.string.fa_cloud_arrow_down),
fontFamily = FontAwesome,
) )
}, },
leadingContent = { onClick = onClickLoad,
Icon(
imageVector = Icons.Default.Create,
contentDescription = null,
)
},
onClick = onClickSaveLocal,
modifier = Modifier, modifier = Modifier,
) )
} }
item { item {
Text(
text = stringResource(R.string.home_rows),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
HorizontalDivider() HorizontalDivider()
} }
itemsIndexed(state.rows, key = { _, row -> row.id }) { index, row -> itemsIndexed(state.rows, key = { _, row -> row.id }) { index, row ->
@ -192,8 +218,7 @@ fun HomeRowConfigContent(
Text( Text(
text = config.title, text = config.title,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = modifier = Modifier,
Modifier,
) )
}, },
onClick = onClick, onClick = onClick,

View file

@ -339,6 +339,52 @@ class HomeSettingsViewModel
} }
} }
fun saveToRemote() {
viewModelScope.launchIO {
serverRepository.currentUser.value?.let { user ->
Timber.d("Saving home settings to remote")
val rows = state.value.rows.map { it.config }
val settings = HomePageSettings(rows = rows)
try {
Timber.d("saveToRemote")
homeSettingsService.saveToServer(user.id, settings)
} catch (ex: Exception) {
Timber.e(ex)
showToast(context, "Error saving: ${ex.localizedMessage}")
}
}
}
}
fun loadFromRemote() {
viewModelScope.launchIO {
serverRepository.currentUser.value?.let { user ->
Timber.d("Loading home settings from remote")
try {
_state.update { it.copy(loading = LoadingState.Loading) }
val result = homeSettingsService.loadFromServer(user.id)
if (result != null) {
Timber.v("Got remote settings")
val newRows =
result.rows.mapIndexed { index, config ->
homeSettingsService.resolve(index, config)
}
_state.update {
it.copy(rows = newRows)
}
} else {
Timber.v("No remote settings")
showToast(context, "No server-side settings found")
}
fetchRowData()
} catch (ex: Exception) {
Timber.e(ex)
showToast(context, "Error: ${ex.localizedMessage}")
}
}
}
}
fun saveToLocal() { fun saveToLocal() {
// This uses injected ioScope so that it will still run when the page is closing // This uses injected ioScope so that it will still run when the page is closing
ioScope.launchIO { ioScope.launchIO {

View file

@ -50,4 +50,6 @@
<string name="fa_clock" translatable="false">&#xf017;</string> <string name="fa_clock" translatable="false">&#xf017;</string>
<string name="fa_bell" translatable="false">&#xf0f3;</string> <string name="fa_bell" translatable="false">&#xf0f3;</string>
<string name="fa_check" translatable="false">&#xf00c;</string> <string name="fa_check" translatable="false">&#xf00c;</string>
<string name="fa_cloud_arrow_up" translatable="false">&#xf0ee;</string>
<string name="fa_cloud_arrow_down" translatable="false">&#xf0ed;</string>
</resources> </resources>

View file

@ -468,6 +468,9 @@
<string name="height">Height</string> <string name="height">Height</string>
<string name="apply_all_rows">Apply to all rows</string> <string name="apply_all_rows">Apply to all rows</string>
<string name="customize_home">Customize home page</string> <string name="customize_home">Customize home page</string>
<string name="home_rows">Home rows</string>
<string name="load_from_server">Load from server</string>
<string name="save_to_server">Save to server</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>