mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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
This commit is contained in:
parent
23856ffcf8
commit
8ea6bbbfca
13 changed files with 497 additions and 134 deletions
|
|
@ -202,18 +202,6 @@ sealed interface AppPreference<Pref, T> {
|
|||
},
|
||||
)
|
||||
|
||||
val CombineContinueNext =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
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<AppPreferences>(
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ val VideoSortOptions =
|
|||
|
||||
val PlaylistSortOptions =
|
||||
listOf(
|
||||
ItemSortBy.DEFAULT,
|
||||
ItemSortBy.SORT_NAME,
|
||||
ItemSortBy.PREMIERE_DATE,
|
||||
ItemSortBy.DATE_CREATED,
|
||||
|
|
|
|||
|
|
@ -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<SearchForViewModel, SearchForViewModel.Factory>(
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui.main.settings
|
||||
|
||||
sealed interface HomeRowConfigAction {
|
||||
data object Combine : HomeRowConfigAction
|
||||
|
||||
data object Split : HomeRowConfigAction
|
||||
}
|
||||
|
|
@ -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<PreferenceGroup<HomeRowViewOptions>>,
|
||||
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)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -820,4 +820,6 @@
|
|||
</array>
|
||||
<string name="stars">Stars</string>
|
||||
<string name="tomatoes">Tomatoes</string>
|
||||
<string name="sort">Sort</string>
|
||||
<string name="split_into_separate_rows">Split into separate rows</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue