Some fixes to customize homes (#893)

## Description
- Update header when editing a row
- Exclude non-empty rows to prevent scroll lock
- Only save settings locally if they are different from the source

### Related issues
Related to #803 

### Testing
Emulator, tested remote & local settings changes

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-13 16:30:03 -05:00 committed by GitHub
parent aa69646b2d
commit 1e810c1157
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 67 additions and 25 deletions

View file

@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
@ -149,8 +150,10 @@ fun RecommendedContent(
} }
LoadingState.Success -> { LoadingState.Success -> {
var position by rememberPosition()
HomePageContent( HomePageContent(
homeRows = rows, homeRows = rows,
position = position,
onClickItem = { _, item -> onClickItem = { _, item ->
viewModel.navigationManager.navigateTo(item.destination()) viewModel.navigationManager.navigateTo(item.destination())
}, },
@ -160,7 +163,21 @@ fun RecommendedContent(
onClickPlay = { _, item -> onClickPlay = { _, item ->
viewModel.navigationManager.navigateTo(Destination.Playback(item)) viewModel.navigationManager.navigateTo(Destination.Playback(item))
}, },
onFocusPosition = onFocusPosition, onFocusPosition = {
position = it
val nonEmptyRowBefore =
rows
.subList(0, it.row)
.count {
it is HomeRowLoadingState.Success && it.items.isEmpty()
}
onFocusPosition?.invoke(
RowColumn(
it.row - nonEmptyRowBefore,
it.column,
),
)
},
showClock = preferences.appPreferences.interfacePreferences.showClock, showClock = preferences.appPreferences.interfacePreferences.showClock,
onUpdateBackdrop = viewModel::updateBackdrop, onUpdateBackdrop = viewModel::updateBackdrop,
modifier = modifier, modifier = modifier,

View file

@ -117,12 +117,17 @@ fun HomePage(
var dialog by remember { mutableStateOf<DialogParams?>(null) } var dialog by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) } var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var position by rememberPosition()
HomePageContent( HomePageContent(
homeRows = homeRows, homeRows = homeRows,
onClickItem = { position, item -> position = position,
onFocusPosition = { position = it },
onClickItem = { clickedPosition, item ->
position = clickedPosition
viewModel.navigationManager.navigateTo(item.destination()) viewModel.navigationManager.navigateTo(item.destination())
}, },
onLongClickItem = { position, item -> onLongClickItem = { clickedPosition, item ->
position = clickedPosition
val dialogItems = val dialogItems =
buildMoreDialogItemsForHome( buildMoreDialogItemsForHome(
context = context, context = context,
@ -193,19 +198,19 @@ fun HomePage(
@Composable @Composable
fun HomePageContent( fun HomePageContent(
homeRows: List<HomeRowLoadingState>, homeRows: List<HomeRowLoadingState>,
position: RowColumn,
onFocusPosition: (RowColumn) -> Unit,
onClickItem: (RowColumn, BaseItem) -> Unit, onClickItem: (RowColumn, BaseItem) -> Unit,
onLongClickItem: (RowColumn, BaseItem) -> Unit, onLongClickItem: (RowColumn, BaseItem) -> Unit,
onClickPlay: (RowColumn, BaseItem) -> Unit, onClickPlay: (RowColumn, BaseItem) -> Unit,
showClock: Boolean, showClock: Boolean,
onUpdateBackdrop: (BaseItem) -> Unit, onUpdateBackdrop: (BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null,
loadingState: LoadingState? = null, loadingState: LoadingState? = null,
listState: LazyListState = rememberLazyListState(), listState: LazyListState = rememberLazyListState(),
takeFocus: Boolean = true, takeFocus: Boolean = true,
showEmptyRows: Boolean = false, showEmptyRows: Boolean = false,
) { ) {
var position by rememberPosition()
val focusedItem = val focusedItem =
position.let { position.let {
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column) (homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
@ -329,21 +334,8 @@ fun HomePageContent(
cardModifier cardModifier
.onFocusChanged { .onFocusChanged {
if (it.isFocused) { if (it.isFocused) {
position = onFocusPosition?.invoke(
RowColumn(rowIndex, index) RowColumn(rowIndex, index),
}
if (it.isFocused && onFocusPosition != null) {
val nonEmptyRowBefore =
homeRows
.subList(0, rowIndex)
.count {
it is HomeRowLoadingState.Success && it.items.isEmpty()
}
onFocusPosition.invoke(
RowColumn(
rowIndex - nonEmptyRowBefore,
index,
),
) )
} }
}.onKeyEvent { }.onKeyEvent {

View file

@ -131,7 +131,14 @@ class HomeViewModel
) )
} }
} }
val rows = deferred.awaitAll() val rows =
deferred
.awaitAll()
.filter {
// Include only errors & non-empty successes
it is HomeRowLoadingState.Error ||
(it is HomeRowLoadingState.Success && it.items.isNotEmpty())
}
Timber.v("Got all rows") Timber.v("Got all rows")
_state.update { _state.update {
it.copy( it.copy(

View file

@ -33,11 +33,14 @@ import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.components.ConfirmDialog import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
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
import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -55,6 +58,7 @@ fun HomeSettingsPage(
var showConfirmDialog by remember { mutableStateOf<ShowConfirm?>(null) } var showConfirmDialog by remember { mutableStateOf<ShowConfirm?>(null) }
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
var position by rememberPosition(0, 0)
// TODO discover rows // TODO discover rows
val discoverEnabled = false // by viewModel.discoverEnabled.collectAsState(false) val discoverEnabled = false // by viewModel.discoverEnabled.collectAsState(false)
@ -109,7 +113,15 @@ fun HomeSettingsPage(
backStack.add(RowSettings(row.id)) backStack.add(RowSettings(row.id))
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
Timber.v("Scroll to $index") Timber.v("Scroll to $index")
listState.scrollToItem(index) listState.animateScrollToItem(index)
// Update backdrop to first item in the row
(state.rowData.getOrNull(index) as? HomeRowLoadingState.Success)
?.items
?.firstOrNull()
?.let {
viewModel.updateBackdrop(it)
}
position = RowColumn(index, 0)
} }
}, },
modifier = destModifier, modifier = destModifier,
@ -259,6 +271,8 @@ fun HomeSettingsPage(
HomePageContent( HomePageContent(
loadingState = state.loading, loadingState = state.loading,
homeRows = state.rowData, homeRows = state.rowData,
position = position,
onFocusPosition = { position = it },
onClickItem = { _, _ -> }, onClickItem = { _, _ -> },
onLongClickItem = { _, _ -> }, onLongClickItem = { _, _ -> },
onClickPlay = { _, _ -> }, onClickPlay = { _, _ -> },

View file

@ -81,6 +81,9 @@ class HomeSettingsViewModel
val discoverEnabled = seerrServerRepository.active val discoverEnabled = seerrServerRepository.active
private var originalLocalSettings: HomePageSettings? = null
private var originalRemoteSettings: HomePageSettings? = null
init { init {
addCloseable { saveToLocal() } addCloseable { saveToLocal() }
viewModelScope.launchIO { viewModelScope.launchIO {
@ -88,6 +91,8 @@ class HomeSettingsViewModel
val libraries = navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess) val libraries = navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess)
val currentSettings = val currentSettings =
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY } homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
originalLocalSettings = homeSettingsService.loadFromLocal(userDto.id)
originalRemoteSettings = homeSettingsService.loadFromServer(userDto.id)
Timber.v("currentSettings=%s", currentSettings) Timber.v("currentSettings=%s", currentSettings)
idCounter = currentSettings.rows.maxOfOrNull { it.id }?.plus(1) ?: 0 idCounter = currentSettings.rows.maxOfOrNull { it.id }?.plus(1) ?: 0
_state.update { _state.update {
@ -493,9 +498,16 @@ class HomeSettingsViewModel
HomePageSettings(rows = rows, SUPPORTED_HOME_PAGE_SETTINGS_VERSION) HomePageSettings(rows = rows, SUPPORTED_HOME_PAGE_SETTINGS_VERSION)
try { try {
Timber.d("saveToLocal") Timber.d("saveToLocal")
val local = homeSettingsService.loadFromLocal(user.id) // Only save if there are changes based on original source
// Only save if there are changes val shouldSave =
if (local != settings) { if (originalLocalSettings != null) {
originalLocalSettings != settings
} else if (originalRemoteSettings != null) {
originalRemoteSettings != settings
} else {
true
}
if (shouldSave) {
homeSettingsService.saveToLocal(user.id, settings) homeSettingsService.saveToLocal(user.id, settings)
homeSettingsService.updateCurrent(settings) homeSettingsService.updateCurrent(settings)
showSaveToast() showSaveToast()