From 8ea6bbbfcaf6de9bd4fe816f0c92667d91d3bc3f Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 26 May 2026 13:30:00 -0400 Subject: [PATCH] Improvements to home page customization (#1458) ## Description Adds a few enhancements to home page customization: - Add option to combined continue watching & next up to split it - Add option to continue watching and next up rows to combine them - Add option for sorting playlist or collections - Show suggestions (recently updated) when searching for a playlist or collection to add - Better handling for loading vs unsupported types for suggestions Also, the old toggle for combining continue watching & next up in advanced settings is removed. Now by default the rows will be combined unless changed in the home page customization settings. ### Related issues Closes #340 Closes #1383 Closes #1350 ### Testing Just on the emulator so far ## Screenshots N/A ## AI or LLM usage None --- .../wholphin/preferences/AppPreference.kt | 13 -- .../preferences/AppPreferencesSerializer.kt | 1 - .../wholphin/services/HomeSettingsService.kt | 103 ++++++------ .../wholphin/ui/components/SortByButton.kt | 40 +++-- .../wholphin/ui/data/SortAndDirection.kt | 1 + .../ui/detail/search/SearchForDialog.kt | 135 ++++++++++----- .../ui/detail/search/SearchForViewModel.kt | 58 ++++++- .../ui/main/settings/HomeRowConfigAction.kt | 7 + .../ui/main/settings/HomeRowSettings.kt | 157 ++++++++++++++++++ .../ui/main/settings/HomeSettingsPage.kt | 16 ++ .../ui/main/settings/HomeSettingsViewModel.kt | 96 +++++++++++ app/src/main/proto/WholphinDataStore.proto | 2 +- app/src/main/res/values/strings.xml | 2 + 13 files changed, 497 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowConfigAction.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 6c92358c..8149f8b6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -202,18 +202,6 @@ sealed interface AppPreference { }, ) - val CombineContinueNext = - AppSwitchPreference( - title = R.string.combine_continue_next, - defaultValue = false, - getter = { it.homePagePreferences.combineContinueNext }, - setter = { prefs, value -> - prefs.updateHomePagePreferences { combineContinueNext = value } - }, - summaryOn = R.string.enabled, - summaryOff = R.string.disabled, - ) - val RewatchNextUp = AppSwitchPreference( title = R.string.rewatch_next_up, @@ -1202,7 +1190,6 @@ val advancedPreferences = AppPreference.BackdropStylePref, AppPreference.ShowLogos, AppPreference.ManageMedia, - AppPreference.CombineContinueNext, AppPreference.DisplayTogglesPref, // Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418 // AppPreference.NavDrawerSwitchOnFocus, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 59b9a6ea..ddac936a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -85,7 +85,6 @@ class AppPreferencesSerializer .apply { maxItemsPerRow = AppPreference.HomePageItems.defaultValue.toInt() enableRewatchingNextUp = AppPreference.RewatchNextUp.defaultValue - combineContinueNext = AppPreference.CombineContinueNext.defaultValue maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt() }.build() interfacePreferences = diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index af3df181..4288e337 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -33,7 +33,6 @@ import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.GetRecordingsRequestHandler import com.github.damontecres.wholphin.util.GetStudiosRequestHandler import com.github.damontecres.wholphin.util.HomeRowLoadingState -import com.github.damontecres.wholphin.util.HomeRowLoadingState.Error import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success import com.github.damontecres.wholphin.util.supportedHomeCollectionTypes import dagger.hilt.android.qualifiers.ApplicationContext @@ -274,9 +273,6 @@ class HomeSettingsService navDrawerService.getAllUserLibraries(userId, userDto?.tvAccess ?: false) } - val prefs = - userPreferencesService.getCurrent().appPreferences.homePagePreferences - val includedIds = libraries .mapIndexed { index, it -> @@ -296,30 +292,15 @@ class HomeSettingsService ) } } - val continueWatchingRows = - if (prefs.combineContinueNext) { - listOf( - HomeRowConfigDisplay( - id = includedIds.size + 1, - title = ResStringProvider(R.string.combine_continue_next), - config = HomeRowConfig.ContinueWatchingCombined(), - ), - ) - } else { - listOf( - HomeRowConfigDisplay( - id = includedIds.size + 1, - title = ResStringProvider(R.string.continue_watching), - config = HomeRowConfig.ContinueWatching(), - ), - HomeRowConfigDisplay( - id = includedIds.size + 2, - title = ResStringProvider(R.string.next_up), - config = HomeRowConfig.NextUp(), - ), - ) - } - val rowConfig = continueWatchingRows + includedIds + val continueWatchingRow = + listOf( + HomeRowConfigDisplay( + id = includedIds.size + 1, + title = ResStringProvider(R.string.combine_continue_next), + config = HomeRowConfig.ContinueWatchingCombined(), + ), + ) + val rowConfig = continueWatchingRow + includedIds return HomePageResolvedSettings(rowConfig) } @@ -864,8 +845,24 @@ class HomeSettingsService userId = userDto.id, parentId = row.parentId, recursive = row.recursive, - sortBy = row.sort?.let { listOf(it.sort) }, - sortOrder = row.sort?.let { listOf(it.direction) }, + sortBy = + row.sort?.let { + buildList { + add(it.sort) + if (it.sort != ItemSortBy.SORT_NAME) { + add(ItemSortBy.SORT_NAME) + } + } + }, + sortOrder = + row.sort?.let { + buildList { + add(it.direction) + if (it.sort != ItemSortBy.SORT_NAME) { + add(SortOrder.ASCENDING) + } + } + }, limit = limit, fields = DefaultItemFields, ) @@ -1090,29 +1087,39 @@ class HomeSettingsService .content val title = ResArgStringProvider(R.string.suggestions_for, library.name ?: "") val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType) - val suggestions = - itemKind?.let { + if (itemKind != null) { + val suggestions = suggestionService .getSuggestionsFlow(row.parentId, itemKind) .firstOrNull() + when (suggestions) { + SuggestionsResource.Empty -> { + Success( + title, + listOf(), + row.viewOptions, + rowType = row, + ) + } + + is SuggestionsResource.Success -> { + Success( + title, + suggestions.items, + row.viewOptions, + rowType = row, + ) + } + + SuggestionsResource.Loading, + null, + -> { + HomeRowLoadingState.Loading(title) + } } - if (suggestions != null && suggestions is SuggestionsResource.Success) { - Success( - title, - suggestions.items, - row.viewOptions, - rowType = row, - ) - } else if (suggestions is SuggestionsResource.Empty) { - Success( - title, - listOf(), - row.viewOptions, - rowType = row, - ) } else { - Error( - title, + HomeRowLoadingState.Error( + title = title, message = "Unsupported type ${library.collectionType}", ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SortByButton.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SortByButton.kt index 405e0fb6..53511a3a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SortByButton.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SortByButton.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -16,11 +15,9 @@ import androidx.compose.ui.text.withStyle import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.FontAwesome -import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.flip import com.github.damontecres.wholphin.ui.data.getStringRes -import com.github.damontecres.wholphin.ui.theme.WholphinTheme import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder @@ -37,10 +34,8 @@ fun SortByButton( modifier: Modifier = Modifier, ) { val currentSort = current.sort - val name = stringResource(getStringRes(currentSort)) val currentDirection = current.direction var sortByDropDown by remember { mutableStateOf(false) } - val context = LocalContext.current Box(modifier = modifier) { TextButton( @@ -50,22 +45,7 @@ fun SortByButton( }, ) { Text( - text = - buildAnnotatedString { - withStyle(SpanStyle(fontFamily = FontAwesome)) { - append( - stringResource( - if (currentDirection == SortOrder.ASCENDING) { - R.string.fa_caret_up - } else { - R.string.fa_caret_down - }, - ), - ) - } - append(" ") - append(name) - }, + text = current.toAnnotatedString(), ) } @@ -117,3 +97,21 @@ fun SortByButton( } } } + +@Composable +fun SortAndDirection.toAnnotatedString() = + buildAnnotatedString { + withStyle(SpanStyle(fontFamily = FontAwesome)) { + append( + stringResource( + if (direction == SortOrder.ASCENDING) { + R.string.fa_caret_up + } else { + R.string.fa_caret_down + }, + ), + ) + } + append(" ") + append(stringResource(getStringRes(sort))) + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt index ee4b8b26..30edee62 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt @@ -73,6 +73,7 @@ val VideoSortOptions = val PlaylistSortOptions = listOf( + ItemSortBy.DEFAULT, ItemSortBy.SORT_NAME, ItemSortBy.PREMIERE_DATE, ItemSortBy.DATE_CREATED, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForDialog.kt index 09df419c..de47d732 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForDialog.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -43,6 +45,7 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.ItemRow +import com.github.damontecres.wholphin.ui.cards.ItemRowTitle import com.github.damontecres.wholphin.ui.cards.SeasonCard import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -57,7 +60,13 @@ fun SearchForContent( searchType: BaseItemKind, onClick: (BaseItem) -> Unit, modifier: Modifier = Modifier, - viewModel: SearchForViewModel = hiltViewModel(key = searchType.serialName), + viewModel: SearchForViewModel = + hiltViewModel( + key = searchType.serialName, + creationCallback = { + it.create(searchType) + }, + ), ) { val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current @@ -92,6 +101,7 @@ fun SearchForContent( } } } + val titleRes = remember { when (searchType) { @@ -178,53 +188,91 @@ fun SearchForContent( } } - when (val st = state.results) { - is SearchResult.Error -> { - ErrorMessage("Error", st.ex) - } - - SearchResult.NoQuery -> { - // no-op - } - - SearchResult.Searching -> { - Text( - text = stringResource(R.string.searching), + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + item { + SearchForResultsRow( + title = stringResource(R.string.results), + results = state.results, + onClick = onClick, + modifier = Modifier.focusRequester(focusRequester), ) } - - is SearchResult.SuccessSeerr -> { - Text( - text = "Not supported", - color = MaterialTheme.colorScheme.error, + item { + HorizontalDivider() + } + item { + SearchForResultsRow( + title = stringResource(R.string.suggestions), + results = state.recent, + onClick = onClick, + modifier = Modifier, ) } + } + } +} - is SearchResult.Success -> { - if (st.items.isEmpty()) { - Text( - text = stringResource(R.string.no_results), - ) - } else { - ItemRow( - title = "", - items = st.items, - onClickItem = { _, item -> onClick.invoke(item) }, - onLongClickItem = { _, _ -> }, - modifier = Modifier.focusRequester(focusRequester), - cardContent = { index, item, mod, onClick, onLongClick -> - SeasonCard( - item = item, - onClick = { - onClick.invoke() - }, - onLongClick = onLongClick, - imageHeight = Cards.height2x3, - modifier = mod, - ) - }, - ) - } +@Composable +fun SearchForResultsRow( + title: String, + results: SearchResult, + onClick: (BaseItem) -> Unit, + modifier: Modifier = Modifier, +) { + when (val st = results) { + is SearchResult.Error -> { + ErrorMessage("Error", st.ex, modifier) + } + + SearchResult.NoQuery -> { + ItemRowTitle( + title = stringResource(R.string.no_results), + modifier = modifier, + ) + } + + SearchResult.Searching -> { + ItemRowTitle( + title = stringResource(R.string.searching), + modifier = modifier, + ) + } + + is SearchResult.SuccessSeerr -> { + Text( + text = "Not supported", + color = MaterialTheme.colorScheme.error, + modifier = modifier, + ) + } + + is SearchResult.Success -> { + if (st.items.isEmpty()) { + ItemRowTitle( + title = stringResource(R.string.no_results), + modifier = modifier, + ) + } else { + ItemRow( + title = title, + items = st.items, + onClickItem = { _, item -> onClick.invoke(item) }, + onLongClickItem = { _, _ -> }, + modifier = modifier, + cardContent = { index, item, mod, onClick, onLongClick -> + SeasonCard( + item = item, + onClick = { + onClick.invoke() + }, + onLongClick = onLongClick, + imageHeight = Cards.heightEpisode, + modifier = mod, + ) + }, + ) } } } @@ -242,6 +290,7 @@ fun SearchForDialog( DialogProperties( usePlatformDefaultWidth = false, ), + elevation = 3.dp, ) { SearchForContent( searchType = searchType, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForViewModel.kt index 245aad05..3ab03300 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/search/SearchForViewModel.kt @@ -8,30 +8,68 @@ import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.components.VoiceInputManager import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.main.SearchResult +import com.github.damontecres.wholphin.ui.toBaseItems import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest import timber.log.Timber -import javax.inject.Inject -@HiltViewModel +@HiltViewModel(assistedFactory = SearchForViewModel.Factory::class) class SearchForViewModel - @Inject + @AssistedInject constructor( private val api: ApiClient, private val serverRepository: ServerRepository, val navigationManager: NavigationManager, val voiceInputManager: VoiceInputManager, + @Assisted val searchType: BaseItemKind, ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create(searchType: BaseItemKind): SearchForViewModel + } + val state = MutableStateFlow(SearchForState()) init { state.value = SearchForState() + viewModelScope.launchIO { + try { + val request = + GetItemsRequest( + userId = serverRepository.currentUser?.id, + includeItemTypes = listOf(searchType), + recursive = true, + fields = SlimItemFields, + sortBy = listOf(ItemSortBy.DATE_LAST_CONTENT_ADDED), + sortOrder = listOf(SortOrder.DESCENDING), + limit = 25, + ) + val recent = api.itemsApi.getItems(request).toBaseItems(api, false) + state.update { + it.copy(recent = SearchResult.Success(recent)) + } + } catch (ex: CancellationException) { + throw ex + } catch (ex: Exception) { + Timber.e(ex, "Error fetching recent %s", searchType) + state.update { + it.copy(recent = SearchResult.Error(ex)) + } + } + } } fun search( @@ -41,10 +79,10 @@ class SearchForViewModel viewModelScope.launchIO { if (state.value.query != query) { if (query.isBlank()) { - state.update { SearchForState(query, SearchResult.NoQuery) } + state.update { it.copy(query = query, results = SearchResult.NoQuery) } return@launchIO } - state.update { SearchForState(query, SearchResult.Searching) } + state.update { it.copy(query = query, results = SearchResult.NoQuery) } try { val request = GetItemsRequest( @@ -55,10 +93,15 @@ class SearchForViewModel fields = SlimItemFields, ) val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init() - state.update { SearchForState(query, SearchResult.Success(pager)) } + state.update { + it.copy( + query = query, + results = SearchResult.Success(pager), + ) + } } catch (ex: Exception) { Timber.e(ex) - state.update { SearchForState(query, SearchResult.Error(ex)) } + state.update { it.copy(query = query, results = SearchResult.Error(ex)) } } } } @@ -67,5 +110,6 @@ class SearchForViewModel data class SearchForState( val query: String = "", + val recent: SearchResult = SearchResult.Searching, val results: SearchResult = SearchResult.NoQuery, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowConfigAction.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowConfigAction.kt new file mode 100644 index 00000000..2a44c8f4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowConfigAction.kt @@ -0,0 +1,7 @@ +package com.github.damontecres.wholphin.ui.main.settings + +sealed interface HomeRowConfigAction { + data object Combine : HomeRowConfigAction + + data object Split : HomeRowConfigAction +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowSettings.kt index 7ac43548..e9f7f6e2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowSettings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeRowSettings.kt @@ -1,17 +1,32 @@ package com.github.damontecres.wholphin.ui.main.settings +import android.view.Gravity import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed 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 +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.DialogWindowProvider +import androidx.tv.material3.ListItem +import androidx.tv.material3.Text import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.preferences.AppChoicePreference import com.github.damontecres.wholphin.preferences.AppClickablePreference @@ -21,18 +36,31 @@ import com.github.damontecres.wholphin.preferences.AppSwitchPreference import com.github.damontecres.wholphin.preferences.PrefContentScale import com.github.damontecres.wholphin.ui.AspectRatio import com.github.damontecres.wholphin.ui.Cards +import com.github.damontecres.wholphin.ui.FontAwesome +import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.ViewOptionImageType +import com.github.damontecres.wholphin.ui.components.toAnnotatedString +import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions +import com.github.damontecres.wholphin.ui.data.SortAndDirection +import com.github.damontecres.wholphin.ui.data.getStringRes import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.preferences.ClickPreference import com.github.damontecres.wholphin.ui.preferences.ComposablePreference import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup +import com.github.damontecres.wholphin.ui.preferences.PreferenceTitle import com.github.damontecres.wholphin.ui.tryRequestFocus +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder @Composable fun HomeRowSettings( title: String, + config: HomeRowConfig, preferenceOptions: List>, viewOptions: HomeRowViewOptions, onViewOptionsChange: (HomeRowViewOptions) -> Unit, + onConfigChange: (HomeRowConfig) -> Unit, + onConfigAction: (HomeRowConfigAction) -> Unit, onApplyApplyAll: () -> Unit, modifier: Modifier = Modifier, defaultViewOptions: HomeRowViewOptions = HomeRowViewOptions(), @@ -95,6 +123,135 @@ fun HomeRowSettings( ) } } + addAdditionalConfig( + config = config, + onConfigChange = onConfigChange, + onConfigAction = onConfigAction, + ) + } + } +} + +private fun LazyListScope.addAdditionalConfig( + config: HomeRowConfig, + onConfigChange: (HomeRowConfig) -> Unit, + onConfigAction: (HomeRowConfigAction) -> Unit, +) { + when (config) { + is HomeRowConfig.ContinueWatchingCombined -> { + item { + ClickPreference( + title = stringResource(R.string.split_into_separate_rows), + onClick = { onConfigAction.invoke(HomeRowConfigAction.Split) }, + ) + } + } + + is HomeRowConfig.ContinueWatching, + is HomeRowConfig.NextUp, + -> { + item { + ClickPreference( + title = stringResource(R.string.combine_continue_next), + onClick = { onConfigAction.invoke(HomeRowConfigAction.Combine) }, + ) + } + } + + is HomeRowConfig.ByParent -> { + item { + val sortAndDirection = + remember(config) { + config.sort ?: SortAndDirection( + ItemSortBy.DEFAULT, + SortOrder.ASCENDING, + ) + } + var showDialog by remember { mutableStateOf(false) } + ListItem( + selected = false, + onClick = { showDialog = true }, + headlineContent = { + PreferenceTitle(stringResource(R.string.sort)) + }, + supportingContent = { + Text( + text = sortAndDirection.toAnnotatedString(), + ) + }, + ) + if (showDialog) { + BasicDialog( + onDismissRequest = { showDialog = false }, + properties = DialogProperties(usePlatformDefaultWidth = false), + elevation = 5.dp, + ) { + val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider + dialogWindowProvider?.window?.let { window -> + window.setGravity(Gravity.START) + } + ByParentSortDialogContent( + current = sortAndDirection, + onClick = { + val newSort = + if (config.sort?.sort == it) { + config.sort.flip() + } else if (it == ItemSortBy.DEFAULT) { + null + } else { + SortAndDirection(it, SortOrder.ASCENDING) + } + onConfigChange.invoke(config.copy(sort = newSort)) + showDialog = false + }, + modifier = Modifier.width(320.dp), + ) + } + } + } + } + + else -> {} + } +} + +@Composable +fun ByParentSortDialogContent( + current: SortAndDirection, + onClick: (ItemSortBy) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + contentPadding = PaddingValues(8.dp), + modifier = modifier, + ) { + items(BoxSetSortOptions) { sort -> + ListItem( + selected = sort == current.sort, + onClick = { onClick.invoke(sort) }, + leadingContent = { + Text( + fontFamily = FontAwesome, + text = + if (sort == current.sort) { + stringResource( + if (current.direction == SortOrder.ASCENDING) { + R.string.fa_caret_up + } else { + R.string.fa_caret_down + }, + ) + } else { + "" + }, + ) + }, + headlineContent = { + Text( + text = stringResource(getStringRes(sort)), + ) + }, + ) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt index e46be8bd..582bd69d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt @@ -232,6 +232,22 @@ fun HomeSettingsPage( viewModel.updateViewOptionsForAll(row.config.viewOptions) }, modifier = destModifier, + config = row.config, + onConfigChange = { + viewModel.onConfigChange(row, it) + }, + onConfigAction = { + viewModel.onConfigAction(row, it) + when (it) { + HomeRowConfigAction.Combine, + HomeRowConfigAction.Split, + -> { + backStack.removeAt( + backStack.lastIndex, + ) + } + } + }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt index cd809e76..ee8fa341 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt @@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope import com.github.damontecres.wholphin.services.tvAccess import com.github.damontecres.wholphin.ui.AspectRatio +import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.util.ResArgStringProvider @@ -782,6 +783,101 @@ class HomeSettingsViewModel } } } + + fun onConfigAction( + row: HomeRowConfigDisplay, + action: HomeRowConfigAction, + ) { + viewModelScope.launchDefault { + when (action) { + HomeRowConfigAction.Combine -> { + val index = + state.value.rows + .indexOf(row) + .coerceAtLeast(0) + val rowsToRemove = + state.value.rows + .filter { it.config is ContinueWatching || it.config is NextUp } + + updateState { + it.copy( + loading = LoadingState.Loading, + rows = + it.rows.toMutableList().apply { + set( + index, + HomeRowConfigDisplay( + id = it.rows[index].id, + title = ResStringProvider(R.string.combine_continue_next), + config = ContinueWatchingCombined(row.config.viewOptions), + ), + ) + removeAll(rowsToRemove) + }, + ) + } + fetchRowData() + } + + HomeRowConfigAction.Split -> { + val index = + state.value.rows + .indexOf(row) + .coerceAtLeast(0) + updateState { + it.copy( + loading = LoadingState.Loading, + rows = + it.rows.toMutableList().apply { + set( + index, + HomeRowConfigDisplay( + id = it.rows[index].id, + title = ResStringProvider(R.string.continue_watching), + config = ContinueWatching(row.config.viewOptions), + ), + ) + add( + index + 1, + HomeRowConfigDisplay( + id = idCounter++, + title = ResStringProvider(R.string.next_up), + config = NextUp(row.config.viewOptions), + ), + ) + }, + ) + } + fetchRowData() + } + } + } + } + + fun onConfigChange( + row: HomeRowConfigDisplay, + config: HomeRowConfig, + ) { + viewModelScope.launchDefault { + val index = + state.value.rows + .indexOf(row) + .coerceAtLeast(0) + updateState { + it.copy( + loading = LoadingState.Loading, + rows = + it.rows.toMutableList().apply { + set( + index, + row.copy(config = config), + ) + }, + ) + } + fetchRowData() + } + } } data class HomePageSettingsState( diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index f4ca3f98..bf98744a 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -92,7 +92,7 @@ message PlaybackPreferences { message HomePagePreferences{ int32 max_items_per_row = 1; bool enable_rewatching_next_up = 2; - bool combine_continue_next = 3; + bool combine_continue_next = 3 [deprecated = true]; int32 max_days_next_up = 4; bool click_to_play = 5; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 119a4a84..dd511f29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -820,4 +820,6 @@ Stars Tomatoes + Sort + Split into separate rows