From b78f5fcc77c44b82c4d08cb1ba8bd5b2769fc41f Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 11 Jan 2026 19:43:21 -0500 Subject: [PATCH 1/3] Better support for remote media (HLS/DASH) (#675) ## Description Check if playback is for a remote source and, if so, play that remote URL directly. Works for both ExoPlayer & MPV. Also adds support for direct playback for DASH in ExoPlayer. ### Related issues Closes #471 Closes #666 --- app/build.gradle.kts | 1 + .../wholphin/ui/playback/PlaybackViewModel.kt | 29 ++++++++++++++----- .../wholphin/util/profile/Codec.kt | 1 + .../util/profile/DeviceProfileUtils.kt | 1 + gradle/libs.versions.toml | 1 + 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c83d76a6..a235cd82 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -199,6 +199,7 @@ dependencies { implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.datasource.okhttp) implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui.compose) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index fe72d7e4..8d4954a6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.Tracks @@ -46,6 +47,7 @@ import com.github.damontecres.wholphin.services.PlaylistCreationResult import com.github.damontecres.wholphin.services.PlaylistCreator import com.github.damontecres.wholphin.services.RefreshRateService import com.github.damontecres.wholphin.services.StreamChoiceService +import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.onMain @@ -61,6 +63,7 @@ import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.checkForSupport import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile +import com.github.damontecres.wholphin.util.profile.Codec import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.supportItemKinds import dagger.hilt.android.lifecycle.HiltViewModel @@ -604,13 +607,18 @@ class PlaybackViewModel source?.let { source -> val mediaUrl = if (source.supportsDirectPlay) { - api.videosApi.getVideoStreamUrl( - itemId = itemId, - mediaSourceId = source.id, - static = true, - tag = source.eTag, - playSessionId = response.playSessionId, - ) + if (source.isRemote && source.path.isNotNullOrBlank()) { + Timber.i("Playback is remote for source: %s", source.id) + source.path + } else { + api.videosApi.getVideoStreamUrl( + itemId = itemId, + mediaSourceId = source.id, + static = true, + tag = source.eTag, + playSessionId = response.playSessionId, + ) + } } else if (source.supportsDirectStream) { source.transcodingUrl?.let(api::createUrl) } else { @@ -663,7 +671,12 @@ class PlaybackViewModel .setMediaId(itemId.toString()) .setUri(mediaUrl.toUri()) .setSubtitleConfigurations(listOfNotNull(externalSubtitle)) - .build() + .apply { + when (source.container) { + Codec.Container.HLS -> setMimeType(MimeTypes.APPLICATION_M3U8) + Codec.Container.DASH -> setMimeType(MimeTypes.APPLICATION_MPD) + } + }.build() val playback = CurrentPlayback( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/Codec.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/Codec.kt index 03318879..ad661617 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/Codec.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/Codec.kt @@ -7,6 +7,7 @@ object Codec { const val `3GP` = "3gp" const val ASF = "asf" const val AVI = "avi" + const val DASH = "dash" const val DVR_MS = "dvr-ms" const val HLS = "hls" const val M2V = "m2v" diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index b7d28db1..6203cc17 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -156,6 +156,7 @@ fun createDeviceProfile( container( Codec.Container.ASF, + Codec.Container.DASH, Codec.Container.HLS, Codec.Container.M4V, Codec.Container.MKV, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57b03009..73f5a287 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -86,6 +86,7 @@ androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasourc androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" } androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" } +androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" } From 197265e7f546633656e9cf76c4be27d9f81f9dfa Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 12 Jan 2026 03:11:25 +0100 Subject: [PATCH 2/3] Add option to always direct play Dolby Vision Profile 7 (#670) ## Description A few devices, like the Nvidia Shield Pro 2019, are able to correctly handle Dolby Vision Profile 7. However, when a Full Enhancement Layer (FEL) is present, these devices effectively ignore the enhancement layer and only apply the RPU metadata. Due to this behavior, device capabilities are reported as not supporting `DOVIWithEL`, which causes the Jellyfin server to strip the DV layer entirely with a remux. This commit adds an option to force direct play of Dolby Vision Profile 7 content, preventing the server from stripping the Dolby Vision layer and allowing compatible devices to handle playback as intended. ### Related issues Fixes https://github.com/damontecres/Wholphin/issues/449 ### Screenshots dovi7 ### AI/LLM usage None Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com> --- .../wholphin/preferences/AppPreference.kt | 12 ++++++++++++ .../wholphin/services/DeviceProfileService.kt | 3 +++ .../wholphin/util/profile/DeviceProfileUtils.kt | 9 ++++++--- app/src/main/proto/WholphinDataStore.proto | 1 + app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 24 insertions(+), 3 deletions(-) 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 4b08b226..5ed8e6dc 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 @@ -414,6 +414,17 @@ sealed interface AppPreference { summaryOff = R.string.disabled, ) + val DirectPlayDoviProfile7 = + AppSwitchPreference( + title = R.string.force_dovi_profile_7, + defaultValue = false, + getter = { it.playbackPreferences.overrides.directPlayDolbyVisionEL }, + setter = { prefs, value -> + prefs.updatePlaybackOverrides { directPlayDolbyVisionEL = value } + }, + summary = R.string.force_dovi_profile_7_summary, + ) + val RememberSelectedTab = AppSwitchPreference( title = R.string.remember_selected_tab, @@ -984,6 +995,7 @@ val advancedPreferences = AppPreference.Ac3Supported, AppPreference.DirectPlayAss, AppPreference.DirectPlayPgs, + AppPreference.DirectPlayDoviProfile7, ), ), ConditionalPreferences( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt index 9ce70b21..95cebb3c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt @@ -42,6 +42,7 @@ class DeviceProfileService downMixAudio = prefs.overrides.downmixStereo, assDirectPlay = prefs.overrides.directPlayAss, pgsDirectPlay = prefs.overrides.directPlayPgs, + dolbyVisionELDirectPlay = prefs.overrides.directPlayDolbyVisionEL, jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0), ) @@ -55,6 +56,7 @@ class DeviceProfileService downMixAudio = newConfig.downMixAudio, assDirectPlay = newConfig.assDirectPlay, pgsDirectPlay = newConfig.pgsDirectPlay, + dolbyVisionELDirectPlay = newConfig.dolbyVisionELDirectPlay, jellyfinTenEleven = newConfig.jellyfinTenEleven, ) } @@ -72,5 +74,6 @@ data class DeviceProfileConfiguration( val downMixAudio: Boolean, val assDirectPlay: Boolean, val pgsDirectPlay: Boolean, + val dolbyVisionELDirectPlay: Boolean, val jellyfinTenEleven: Boolean, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index 6203cc17..0f507fd3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -66,6 +66,7 @@ fun createDeviceProfile( downMixAudio: Boolean, assDirectPlay: Boolean, pgsDirectPlay: Boolean, + dolbyVisionELDirectPlay: Boolean, jellyfinTenEleven: Boolean, ) = buildDeviceProfile { val allowedAudioCodecs = @@ -428,9 +429,11 @@ fun createDeviceProfile( if (jellyfinTenEleven) add("DOVIInvalid") if (!supportsHevcDolbyVisionEL) { - if (jellyfinTenEleven) { - add("DOVIWithEL") - if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus") + if (!dolbyVisionELDirectPlay) { + if (jellyfinTenEleven) { + add("DOVIWithEL") + if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus") + } } if (!supportsHevcDolbyVision) { diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index e4cd52d7..f68ecae8 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -45,6 +45,7 @@ message PlaybackOverrides{ bool direct_play_ass = 3; bool direct_play_pgs = 4; MediaExtensionStatus media_extensions_enabled = 5; + bool direct_play_dolby_vision_e_l = 6; } message PlaybackPreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9fc0ed5..90676968 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -410,6 +410,8 @@ DD DD+ DTS + Direct play Dolby Vision Profile 7 + Ignores device compatibility checks Disabled From d5192abf62cc3234e2749c29b8aa8b6a6becfaed Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:36:24 -0500 Subject: [PATCH 3/3] Return focus to the tabs if a grid has no results (#676) ## Description Previously, if you clicked on a tab and the resulting grid had no results, the nav drawer would open and focus which is not good. This PR fixes that and instead returns focus back to the tab row. ### Related issues Fixes #543 --- .../ui/components/CollectionFolderGrid.kt | 12 ++++++++- .../wholphin/ui/components/TabRow.kt | 7 +++++- .../ui/detail/CollectionFolderLiveTv.kt | 25 +++++++++++++------ .../ui/detail/CollectionFolderMovie.kt | 4 +++ .../wholphin/ui/detail/CollectionFolderTv.kt | 3 +++ .../wholphin/ui/detail/FavoritesPage.kt | 8 ++++++ .../wholphin/ui/detail/livetv/DvrSchedule.kt | 9 ++++++- .../ui/detail/series/SeriesOverviewContent.kt | 20 ++++++++++----- 8 files changed, 71 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 20270b94..fefb4d94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -86,6 +86,7 @@ import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.toServerString +import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -525,6 +526,7 @@ fun CollectionFolderGrid( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, useSeriesForPrimary: Boolean = true, filterOptions: List> = DefaultFilterOptions, + focusRequesterOnEmpty: FocusRequester? = null, ) = CollectionFolderGrid( preferences, itemId.toServerString(), @@ -541,6 +543,7 @@ fun CollectionFolderGrid( positionCallback = positionCallback, useSeriesForPrimary = useSeriesForPrimary, filterOptions = filterOptions, + focusRequesterOnEmpty = focusRequesterOnEmpty, ) @Composable @@ -560,6 +563,7 @@ fun CollectionFolderGrid( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, useSeriesForPrimary: Boolean = true, filterOptions: List> = DefaultFilterOptions, + focusRequesterOnEmpty: FocusRequester? = null, playlistViewModel: AddPlaylistViewModel = hiltViewModel(), viewModel: CollectionFolderViewModel = hiltViewModel( @@ -615,6 +619,7 @@ fun CollectionFolderGrid( pager = pager, sortAndDirection = sortAndDirection!!, modifier = Modifier.fillMaxSize(), + focusRequesterOnEmpty = focusRequesterOnEmpty, onClickItem = onClickItem, onLongClickItem = { position, item -> moreDialog.makePresent(PositionItem(position, item)) @@ -759,6 +764,7 @@ fun CollectionFolderGridContent( currentFilter: GetItemsFilter = GetItemsFilter(), filterOptions: List> = listOf(), onFilterChange: (GetItemsFilter) -> Unit = {}, + focusRequesterOnEmpty: FocusRequester? = null, ) { val context = LocalContext.current @@ -767,7 +773,11 @@ fun CollectionFolderGridContent( var viewOptions by remember { mutableStateOf(viewOptions) } val gridFocusRequester = remember { FocusRequester() } - RequestOrRestoreFocus(gridFocusRequester) + if (pager.isNotEmpty()) { + RequestOrRestoreFocus(gridFocusRequester) + } else { + focusRequesterOnEmpty?.tryRequestFocus() + } var backdropImageUrl by remember { mutableStateOf(null) } var position by rememberInt(initialPosition) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt index b8607b82..25b51698 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned @@ -39,13 +40,16 @@ import androidx.compose.ui.unit.sp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.ui.PreviewTvSpec +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus +import timber.log.Timber @Composable fun TabRow( selectedTabIndex: Int, tabs: List, + focusRequesters: List, onClick: (Int) -> Unit, modifier: Modifier = Modifier, ) { @@ -55,7 +59,6 @@ fun TabRow( state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt()) } } - val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } } var rowHasFocus by remember { mutableStateOf(false) } LazyRow( state = state, @@ -68,6 +71,7 @@ fun TabRow( onEnter = { // If entering from left or right, use last or first tab // Otherwise use the selected tab + Timber.v("onEnter requestedFocusDirection=$requestedFocusDirection, selectedTabIndex=$selectedTabIndex") val focusRequester = if (requestedFocusDirection == FocusDirection.Left) { focusRequesters.lastOrNull() @@ -183,6 +187,7 @@ private fun TabRowPreview() { TabRow( selectedTabIndex = 1, tabs = listOf("Tab 1", "Tab 2", "Tab 3"), + focusRequesters = listOf(), onClick = {}, ) Tab( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt index c71e0abe..6d1fdd02 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -93,14 +93,19 @@ fun CollectionFolderLiveTv( remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) } val folders by viewModel.recordingFolders.observeAsState(listOf()) + val tvGuideStr = stringResource(R.string.tv_guide) + val tvDvrStr = stringResource(R.string.tv_dvr_schedule) val tabs = - listOf( - TabId(stringResource(R.string.tv_guide), UUID.randomUUID()), - TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()), - ) + folders + remember(folders) { + listOf( + TabId(tvGuideStr, UUID.randomUUID()), + TabId(tvDvrStr, UUID.randomUUID()), + ) + folders + } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val tabFocusRequesters = remember(tabs.size) { List(tabs.size) { FocusRequester() } } val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } @@ -133,6 +138,7 @@ fun CollectionFolderLiveTv( .focusRequester(firstTabFocusRequester), tabs = tabs.map { it.title }, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } when (selectedTabIndex) { @@ -150,10 +156,12 @@ fun CollectionFolderLiveTv( 1 -> { DvrSchedule( - true, - Modifier - .fillMaxSize() - .focusRequester(focusRequester), + requestFocusAfterLoading = true, + focusRequesterOnEmpty = tabFocusRequesters[1], + modifier = + Modifier + .fillMaxSize() + .focusRequester(focusRequester), ) } @@ -176,6 +184,7 @@ fun CollectionFolderLiveTv( }, playEnabled = false, defaultViewOptions = ViewOptions(), + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } else { ErrorMessage("Invalid tab index $selectedTabIndex", null) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index 1292cd5c..e76b54cd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -59,6 +59,7 @@ fun CollectionFolderMovie( ) var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } @@ -88,6 +89,7 @@ fun CollectionFolderMovie( .focusRequester(firstTabFocusRequester), tabs = tabs, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } when (selectedTabIndex) { @@ -136,6 +138,7 @@ fun CollectionFolderMovie( showHeader = position < columns }, playEnabled = true, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -168,6 +171,7 @@ fun CollectionFolderMovie( showHeader = position < columns }, playEnabled = false, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index bf032813..42b13d96 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -58,6 +58,7 @@ fun CollectionFolderTv( ) var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } @@ -91,6 +92,7 @@ fun CollectionFolderTv( .focusRequester(firstTabFocusRequester), tabs = tabs, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } when (selectedTabIndex) { @@ -138,6 +140,7 @@ fun CollectionFolderTv( preferencesViewModel.navigationManager.navigateTo(item.destination()) }, playEnabled = false, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index a62c58f5..fc337d88 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -73,6 +73,7 @@ fun FavoritesPage( stringResource(R.string.playlists), stringResource(R.string.people), ) + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } @@ -107,6 +108,7 @@ fun FavoritesPage( .padding(start = 32.dp, top = 16.dp, bottom = 16.dp), tabs = tabs, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } // TODO playEnabled = true for movies & episodes @@ -139,6 +141,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -170,6 +173,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -202,6 +206,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -233,6 +238,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -264,6 +270,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -300,6 +307,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = listOf(), + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt index cfa7637b..f9758d5f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt @@ -114,6 +114,7 @@ class DvrScheduleViewModel @Composable fun DvrSchedule( requestFocusAfterLoading: Boolean, + focusRequesterOnEmpty: FocusRequester, modifier: Modifier = Modifier, viewModel: DvrScheduleViewModel = hiltViewModel(), ) { @@ -138,7 +139,13 @@ fun DvrSchedule( var showDialog by remember { mutableStateOf(null) } val focusRequester = remember { FocusRequester() } if (requestFocusAfterLoading) { - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + LaunchedEffect(Unit) { + if (active.isNotEmpty() || recordings.isNotEmpty()) { + focusRequester.tryRequestFocus() + } else { + focusRequesterOnEmpty.tryRequestFocus() + } + } } DvrScheduleContent( activeRecordings = active, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 7f346d6c..98698826 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -107,6 +107,18 @@ fun SeriesOverviewContent( val scrollState = rememberScrollState() val scrollConnection = rememberDelayedNestedScroll() var requestFocusAfterSeason by remember { mutableStateOf(false) } + + val seasonStr = stringResource(R.string.tv_season) + val tabs = + remember(seasons) { + seasons.mapNotNull { + it?.name + ?: it?.data?.indexNumber?.let { "$seasonStr $it" } + ?: "" + } + } + val focusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } } + Box( modifier = modifier @@ -138,17 +150,13 @@ fun SeriesOverviewContent( } TabRow( selectedTabIndex = selectedTabIndex, - tabs = - seasons.mapNotNull { - it?.name - ?: it?.data?.indexNumber?.let { stringResource(R.string.tv_season) + " $it" } - ?: "" - }, + tabs = tabs, onClick = { selectedTabIndex = it onChangeSeason.invoke(it) requestFocusAfterSeason = true }, + focusRequesters = focusRequesters, modifier = Modifier .focusRequester(tabRowFocusRequester)