Migrate live tv guide settings to compose

This commit is contained in:
Niels van Velzen
2025-12-31 12:04:53 +01:00
committed by Niels van Velzen
parent 00125664ac
commit ebec491592
12 changed files with 281 additions and 121 deletions

View File

@@ -1,34 +0,0 @@
package org.jellyfin.androidtv.ui.livetv
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.SystemPreferences
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.OptionsItemCheckbox
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.koin.android.ext.android.inject
class GuideFiltersScreen : OptionsFragment() {
private val systemPreferences: SystemPreferences by inject()
override val screen by optionsScreen {
setTitle(R.string.lbl_filters)
category {
setOf(
SystemPreferences.liveTvGuideFilterMovies to getString(R.string.lbl_movies),
SystemPreferences.liveTvGuideFilterSeries to getString(R.string.lbl_series),
SystemPreferences.liveTvGuideFilterNews to getString(R.string.lbl_news),
SystemPreferences.liveTvGuideFilterKids to getString(R.string.lbl_kids),
SystemPreferences.liveTvGuideFilterSports to getString(R.string.lbl_sports),
SystemPreferences.liveTvGuideFilterPremiere to getString(R.string.lbl_new_only),
).forEach { (preference, label) ->
checkbox {
title = label
type = OptionsItemCheckbox.Type.SWITCH
bind(systemPreferences, preference)
}
}
}
}
}

View File

@@ -1,63 +0,0 @@
package org.jellyfin.androidtv.ui.livetv
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.LiveTvPreferences
import org.jellyfin.androidtv.preference.constant.LiveTvChannelOrder
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.OptionsItemCheckbox
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.enum
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.jellyfin.preference.store.PreferenceStore
import org.koin.android.ext.android.inject
class GuideOptionsScreen : OptionsFragment() {
private val liveTvPreferences: LiveTvPreferences by inject()
override val stores: Array<PreferenceStore<*, *>>
get() = arrayOf(liveTvPreferences)
override val screen by optionsScreen {
setTitle(R.string.live_tv_preferences)
category {
enum<LiveTvChannelOrder> {
title = getString(R.string.lbl_sort_by)
bind {
get { LiveTvChannelOrder.fromString(liveTvPreferences[LiveTvPreferences.channelOrder]) }
set { liveTvPreferences[LiveTvPreferences.channelOrder] = it.stringValue }
default { LiveTvChannelOrder.fromString(liveTvPreferences.getDefaultValue(LiveTvPreferences.channelOrder)) }
}
}
checkbox {
title = getString(R.string.lbl_start_favorites)
bind(liveTvPreferences, LiveTvPreferences.favsAtTop)
}
checkbox {
title = getString(R.string.lbl_colored_backgrounds)
bind(liveTvPreferences, LiveTvPreferences.colorCodeGuide)
}
}
category {
title = getString(R.string.lbl_show_indicators)
setOf(
LiveTvPreferences.showHDIndicator to getString(R.string.lbl_hd_programs),
LiveTvPreferences.showLiveIndicator to getString(R.string.lbl_live_broadcasts),
LiveTvPreferences.showNewIndicator to getString(R.string.lbl_new_episodes),
LiveTvPreferences.showPremiereIndicator to getString(R.string.lbl_premieres),
LiveTvPreferences.showRepeatIndicator to getString(R.string.lbl_repeat_episodes),
).forEach { (preference, label) ->
checkbox {
title = label
type = OptionsItemCheckbox.Type.SWITCH
bind(liveTvPreferences, preference)
}
}
}
}
}

View File

@@ -39,7 +39,6 @@ import org.jellyfin.androidtv.ui.ObservableHorizontalScrollView;
import org.jellyfin.androidtv.ui.ObservableScrollView;
import org.jellyfin.androidtv.ui.ProgramGridCell;
import org.jellyfin.androidtv.ui.ScrollViewListener;
import org.jellyfin.androidtv.ui.navigation.ActivityDestinations;
import org.jellyfin.androidtv.ui.playback.PlaybackLauncher;
import org.jellyfin.androidtv.util.CoroutineUtils;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
@@ -59,6 +58,7 @@ import java.util.List;
import java.util.UUID;
import kotlin.Lazy;
import kotlinx.coroutines.flow.MutableStateFlow;
import timber.log.Timber;
public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.OnKeyListener {
@@ -105,6 +105,8 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
private final Lazy<PlaybackHelper> playbackHelper = inject(PlaybackHelper.class);
private final Lazy<ImageHelper> imageHelper = inject(ImageHelper.class);
private final Lazy<PlaybackLauncher> playbackLauncher = inject(PlaybackLauncher.class);
private MutableStateFlow<Boolean> showOptions;
private MutableStateFlow<Boolean> showFilterOptions;
@Nullable
@Override
@@ -129,20 +131,22 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
mSpinner = binding.spinner;
mSpinner.setVisibility(View.VISIBLE);
showFilterOptions = LiveTvGuideFragmentHelperKt.addSettingsFilters(this, binding);
View mFilterButton = binding.filterButton;
mFilterButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showFilterOptions();
showFilterOptions.setValue(true);
}
});
mFilterButton.setContentDescription(getString(R.string.lbl_filters));
showOptions = LiveTvGuideFragmentHelperKt.addSettingsOptions(this, binding);
View mOptionsButton = binding.optionsButton;
mOptionsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showOptions();
showOptions.setValue(true);
}
});
mOptionsButton.setContentDescription(getString(R.string.lbl_other_options));
@@ -317,7 +321,7 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
switch (keyCode) {
case KeyEvent.KEYCODE_MENU:
// bring up filter selection
showFilterOptions();
showFilterOptions.setValue(true);
break;
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
@@ -451,16 +455,6 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
mDetailPopup.show(mImage, mTitle.getLeft(), mTitle.getTop() - 10);
}
public void showFilterOptions() {
startActivity(ActivityDestinations.INSTANCE.liveTvGuideFilterPreferences(getContext()));
TvManager.forceReload();
}
public void showOptions() {
startActivity(ActivityDestinations.INSTANCE.liveTvGuideOptionPreferences(getContext()));
TvManager.forceReload();
}
public void displayChannels(int start, int max) {
int end = start + max;
if (end > mAllChannels.size()) {

View File

@@ -2,15 +2,25 @@ package org.jellyfin.androidtv.ui.livetv
import android.content.Context
import android.view.View
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
import org.jellyfin.androidtv.databinding.LiveTvGuideBinding
import org.jellyfin.androidtv.ui.GuideChannelHeader
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.navigation.ProvideRouter
import org.jellyfin.androidtv.ui.navigation.RouterContent
import org.jellyfin.androidtv.ui.settings.Routes
import org.jellyfin.androidtv.ui.settings.composable.SettingsDialog
import org.jellyfin.androidtv.ui.settings.routes
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemDto
@@ -74,3 +84,59 @@ fun LiveTvGuideFragment.refreshSelectedProgram() {
detailUpdateInternal();
}
}
fun LiveTvGuideFragment.addSettingsOptions(binding: LiveTvGuideBinding): MutableStateFlow<Boolean> {
val visible = MutableStateFlow(false)
binding.settingsOptions.setContent {
val isVisible by visible.collectAsState(false)
JellyfinTheme {
ProvideRouter(
routes,
Routes.LIVETV_GUIDE_OPTIONS
) {
SettingsDialog(
visible = isVisible,
onDismissRequest = {
visible.value = false
TvManager.forceReload()
doLoad()
}
) {
RouterContent()
}
}
}
}
return visible
}
fun LiveTvGuideFragment.addSettingsFilters(binding: LiveTvGuideBinding): MutableStateFlow<Boolean> {
val visible = MutableStateFlow(false)
binding.settingsFilters.setContent {
val isVisible by visible.collectAsState(false)
JellyfinTheme {
ProvideRouter(
routes,
Routes.LIVETV_GUIDE_FILTERS
) {
SettingsDialog(
visible = isVisible,
onDismissRequest = {
visible.value = false
TvManager.forceReload()
doLoad()
}
) {
RouterContent()
}
}
}
}
return visible
}

View File

@@ -3,8 +3,6 @@ package org.jellyfin.androidtv.ui.navigation
import android.content.Context
import android.content.Intent
import androidx.core.os.bundleOf
import org.jellyfin.androidtv.ui.livetv.GuideFiltersScreen
import org.jellyfin.androidtv.ui.livetv.GuideOptionsScreen
import org.jellyfin.androidtv.ui.playback.ExternalPlayerActivity
import org.jellyfin.androidtv.ui.preference.PreferencesActivity
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
@@ -27,9 +25,6 @@ object ActivityDestinations {
fun playbackAdvancedPreferences(context: Context) = preferenceIntent<PlaybackAdvancedPreferencesScreen>(context)
fun liveTvGuideFilterPreferences(context: Context) = preferenceIntent<GuideFiltersScreen>(context)
fun liveTvGuideOptionPreferences(context: Context) = preferenceIntent<GuideOptionsScreen>(context)
fun externalPlayer(context: Context, position: Duration = Duration.ZERO) = Intent(context, ExternalPlayerActivity::class.java).apply {
putExtras(
bundleOf(

View File

@@ -28,6 +28,9 @@ import org.jellyfin.androidtv.ui.settings.screen.library.SettingsLibrariesDispla
import org.jellyfin.androidtv.ui.settings.screen.library.SettingsLibrariesScreen
import org.jellyfin.androidtv.ui.settings.screen.license.SettingsLicenseScreen
import org.jellyfin.androidtv.ui.settings.screen.license.SettingsLicensesScreen
import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideChannelOrderScreen
import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideFiltersScreen
import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideOptionsScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackInactivityPromptScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackPlayerScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackPrerollsScreen
@@ -69,6 +72,9 @@ object Routes {
const val LIBRARIES_DISPLAY_GRID = "/libraries/display/{itemId}/{displayPreferencesId}/grid"
const val HOME = "/home"
const val HOME_SECTION = "/home/section/{index}"
const val LIVETV_GUIDE_FILTERS = "/livetv/guide/filters"
const val LIVETV_GUIDE_OPTIONS = "/livetv/guide/options"
const val LIVETV_GUIDE_CHANNEL_ORDER = "/livetv/guide/channel-order"
const val PLAYBACK = "/playback"
const val PLAYBACK_PLAYER = "/playback/player"
const val PLAYBACK_NEXT_UP = "/playback/next-up"
@@ -168,6 +174,15 @@ val routes = mapOf<String, RouteComposable>(
Routes.HOME_SECTION to { context ->
SettingsHomeSectionScreen(context.parameters["index"]?.toInt()!!)
},
Routes.LIVETV_GUIDE_FILTERS to {
SettingsLiveTvGuideFiltersScreen()
},
Routes.LIVETV_GUIDE_OPTIONS to {
SettingsLiveTvGuideOptionsScreen()
},
Routes.LIVETV_GUIDE_CHANNEL_ORDER to {
SettingsLiveTvGuideChannelOrderScreen()
},
Routes.PLAYBACK to {
SettingsPlaybackScreen()
},

View File

@@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.flow.map
@@ -15,7 +14,6 @@ import org.jellyfin.androidtv.ui.base.Icon
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.ActivityDestinations
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.Routes
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
@@ -24,7 +22,6 @@ import org.koin.compose.koinInject
@Composable
fun SettingsLibrariesScreen() {
val context = LocalContext.current
val router = LocalRouter.current
val userViewsRepository = koinInject<UserViewsRepository>()
val userViews by remember {
@@ -47,7 +44,7 @@ fun SettingsLibrariesScreen() {
ListButton(
leadingContent = { Icon(painterResource(R.drawable.ic_guide), contentDescription = null) },
headingContent = { Text(userView.name.orEmpty()) },
onClick = { context.startActivity(ActivityDestinations.liveTvGuideOptionPreferences(context = context)) }
onClick = { router.push(Routes.LIVETV_GUIDE_OPTIONS) }
)
} else {
val canOpen = allowGridView && displayPreferencesId != null

View File

@@ -0,0 +1,46 @@
package org.jellyfin.androidtv.ui.settings.screen.livetv
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.LiveTvPreferences
import org.jellyfin.androidtv.preference.constant.LiveTvChannelOrder
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.RadioButton
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsLiveTvGuideChannelOrderScreen() {
val router = LocalRouter.current
val liveTvPreferences = koinInject<LiveTvPreferences>()
var channelOrder by rememberPreference(liveTvPreferences, LiveTvPreferences.channelOrder)
val channelOrderEnum = LiveTvChannelOrder.fromString(channelOrder)
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.settings).uppercase()) },
headingContent = { Text(stringResource(R.string.lbl_sort_by)) },
)
}
items(LiveTvChannelOrder.entries) { entry ->
ListButton(
headingContent = { Text(stringResource(entry.nameRes)) },
trailingContent = { RadioButton(checked = channelOrderEnum == entry) },
onClick = {
channelOrder = entry.stringValue
router.back()
}
)
}
}
}

View File

@@ -0,0 +1,49 @@
package org.jellyfin.androidtv.ui.settings.screen.livetv
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.SystemPreferences
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.Checkbox
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsLiveTvGuideFiltersScreen() {
val systemPreferences = koinInject<SystemPreferences>()
val filters = listOf(
rememberPreference(systemPreferences, SystemPreferences.liveTvGuideFilterMovies) to stringResource(R.string.lbl_movies),
rememberPreference(systemPreferences, SystemPreferences.liveTvGuideFilterSeries) to stringResource(R.string.lbl_series),
rememberPreference(systemPreferences, SystemPreferences.liveTvGuideFilterNews) to stringResource(R.string.lbl_news),
rememberPreference(systemPreferences, SystemPreferences.liveTvGuideFilterKids) to stringResource(R.string.lbl_kids),
rememberPreference(systemPreferences, SystemPreferences.liveTvGuideFilterSports) to stringResource(R.string.lbl_sports),
rememberPreference(systemPreferences, SystemPreferences.liveTvGuideFilterPremiere) to stringResource(R.string.lbl_new_only),
)
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.lbl_live_tv_guide).uppercase()) },
headingContent = { Text(stringResource(R.string.filters)) },
)
}
items(filters) { (preference, label) ->
var enabled by preference
ListButton(
headingContent = { Text(label) },
trailingContent = { Checkbox(checked = enabled) },
onClick = { enabled = !enabled }
)
}
}
}

View File

@@ -0,0 +1,84 @@
package org.jellyfin.androidtv.ui.settings.screen.livetv
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.LiveTvPreferences
import org.jellyfin.androidtv.preference.constant.LiveTvChannelOrder
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.Checkbox
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.Routes
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsLiveTvGuideOptionsScreen() {
val router = LocalRouter.current
val liveTvPreferences = koinInject<LiveTvPreferences>()
val indicators = listOf(
rememberPreference(liveTvPreferences, LiveTvPreferences.showHDIndicator) to stringResource(R.string.lbl_hd_programs),
rememberPreference(liveTvPreferences, LiveTvPreferences.showLiveIndicator) to stringResource(R.string.lbl_live_broadcasts),
rememberPreference(liveTvPreferences, LiveTvPreferences.showNewIndicator) to stringResource(R.string.lbl_new_episodes),
rememberPreference(liveTvPreferences, LiveTvPreferences.showPremiereIndicator) to stringResource(R.string.lbl_premieres),
rememberPreference(liveTvPreferences, LiveTvPreferences.showRepeatIndicator) to stringResource(R.string.lbl_repeat_episodes),
)
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.lbl_live_tv_guide).uppercase()) },
headingContent = { Text(stringResource(R.string.settings)) },
)
}
item {
var channelOrder by rememberPreference(liveTvPreferences, LiveTvPreferences.channelOrder)
ListButton(
headingContent = { Text(stringResource(R.string.lbl_sort_by)) },
captionContent = { Text(stringResource(LiveTvChannelOrder.fromString(channelOrder).nameRes)) },
onClick = { router.push(Routes.LIVETV_GUIDE_CHANNEL_ORDER) }
)
}
item {
var favsAtTop by rememberPreference(liveTvPreferences, LiveTvPreferences.favsAtTop)
ListButton(
headingContent = { Text(stringResource(R.string.lbl_start_favorites)) },
trailingContent = { Checkbox(checked = favsAtTop) },
onClick = { favsAtTop = !favsAtTop }
)
}
item {
var colorCodeGuide by rememberPreference(liveTvPreferences, LiveTvPreferences.colorCodeGuide)
ListButton(
headingContent = { Text(stringResource(R.string.lbl_colored_backgrounds)) },
trailingContent = { Checkbox(checked = colorCodeGuide) },
onClick = { colorCodeGuide = !colorCodeGuide }
)
}
item { ListSection(headingContent = { Text(stringResource(R.string.lbl_show_indicators)) }) }
items(indicators) { (preference, label) ->
var enabled by preference
ListButton(
headingContent = { Text(label) },
trailingContent = { Checkbox(checked = enabled) },
onClick = { enabled = !enabled }
)
}
}
}

View File

@@ -247,4 +247,14 @@
android:layout_marginTop="10sp" />
</RelativeLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/settingsFilters"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/settingsOptions"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

@@ -148,7 +148,7 @@
<string name="msg_unable_load_subs">Unable to load subtitle</string>
<string name="msg_burn_sub_warning">Burning in subtitles. This may take a few seconds…</string>
<string name="lbl_no_recordings">No recordings available</string>
<string name="lbl_live_tv_guide">Live TV Guide</string>
<string name="lbl_live_tv_guide">Live TV guide</string>
<string name="lbl_recorded_tv">Recorded TV</string>
<string name="pref_live_tv_cat">Live TV</string>
<string name="msg_error_live_stream">Error streaming live TV. Will re-try…</string>
@@ -580,6 +580,7 @@
<string name="video_player_internal">Built-in video player</string>
<string name="video_player_external">External app</string>
<string name="playback_video_player_external_empty">You don\'t have any video player apps installed on your device that can open Jellyfin videos.</string>
<string name="filters">Filters</string>
<plurals name="seconds">
<item quantity="one">%1$s second</item>
<item quantity="other">%1$s seconds</item>