mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into develop/jellyseerr
This commit is contained in:
commit
84fb998cc5
17 changed files with 120 additions and 28 deletions
|
|
@ -233,6 +233,7 @@ dependencies {
|
||||||
implementation(libs.androidx.media3.session)
|
implementation(libs.androidx.media3.session)
|
||||||
implementation(libs.androidx.media3.datasource.okhttp)
|
implementation(libs.androidx.media3.datasource.okhttp)
|
||||||
implementation(libs.androidx.media3.exoplayer.hls)
|
implementation(libs.androidx.media3.exoplayer.hls)
|
||||||
|
implementation(libs.androidx.media3.exoplayer.dash)
|
||||||
implementation(libs.androidx.media3.ui)
|
implementation(libs.androidx.media3.ui)
|
||||||
implementation(libs.androidx.media3.ui.compose)
|
implementation(libs.androidx.media3.ui.compose)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,17 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val DirectPlayDoviProfile7 =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
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 =
|
val RememberSelectedTab =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppSwitchPreference<AppPreferences>(
|
||||||
title = R.string.remember_selected_tab,
|
title = R.string.remember_selected_tab,
|
||||||
|
|
@ -992,6 +1003,7 @@ val advancedPreferences =
|
||||||
AppPreference.Ac3Supported,
|
AppPreference.Ac3Supported,
|
||||||
AppPreference.DirectPlayAss,
|
AppPreference.DirectPlayAss,
|
||||||
AppPreference.DirectPlayPgs,
|
AppPreference.DirectPlayPgs,
|
||||||
|
AppPreference.DirectPlayDoviProfile7,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ConditionalPreferences(
|
ConditionalPreferences(
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class DeviceProfileService
|
||||||
downMixAudio = prefs.overrides.downmixStereo,
|
downMixAudio = prefs.overrides.downmixStereo,
|
||||||
assDirectPlay = prefs.overrides.directPlayAss,
|
assDirectPlay = prefs.overrides.directPlayAss,
|
||||||
pgsDirectPlay = prefs.overrides.directPlayPgs,
|
pgsDirectPlay = prefs.overrides.directPlayPgs,
|
||||||
|
dolbyVisionELDirectPlay = prefs.overrides.directPlayDolbyVisionEL,
|
||||||
jellyfinTenEleven =
|
jellyfinTenEleven =
|
||||||
serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
|
serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
|
||||||
)
|
)
|
||||||
|
|
@ -55,6 +56,7 @@ class DeviceProfileService
|
||||||
downMixAudio = newConfig.downMixAudio,
|
downMixAudio = newConfig.downMixAudio,
|
||||||
assDirectPlay = newConfig.assDirectPlay,
|
assDirectPlay = newConfig.assDirectPlay,
|
||||||
pgsDirectPlay = newConfig.pgsDirectPlay,
|
pgsDirectPlay = newConfig.pgsDirectPlay,
|
||||||
|
dolbyVisionELDirectPlay = newConfig.dolbyVisionELDirectPlay,
|
||||||
jellyfinTenEleven = newConfig.jellyfinTenEleven,
|
jellyfinTenEleven = newConfig.jellyfinTenEleven,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -72,5 +74,6 @@ data class DeviceProfileConfiguration(
|
||||||
val downMixAudio: Boolean,
|
val downMixAudio: Boolean,
|
||||||
val assDirectPlay: Boolean,
|
val assDirectPlay: Boolean,
|
||||||
val pgsDirectPlay: Boolean,
|
val pgsDirectPlay: Boolean,
|
||||||
|
val dolbyVisionELDirectPlay: Boolean,
|
||||||
val jellyfinTenEleven: Boolean,
|
val jellyfinTenEleven: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.rememberInt
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
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.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -525,6 +526,7 @@ fun CollectionFolderGrid(
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
useSeriesForPrimary: Boolean = true,
|
useSeriesForPrimary: Boolean = true,
|
||||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||||
|
focusRequesterOnEmpty: FocusRequester? = null,
|
||||||
) = CollectionFolderGrid(
|
) = CollectionFolderGrid(
|
||||||
preferences,
|
preferences,
|
||||||
itemId.toServerString(),
|
itemId.toServerString(),
|
||||||
|
|
@ -541,6 +543,7 @@ fun CollectionFolderGrid(
|
||||||
positionCallback = positionCallback,
|
positionCallback = positionCallback,
|
||||||
useSeriesForPrimary = useSeriesForPrimary,
|
useSeriesForPrimary = useSeriesForPrimary,
|
||||||
filterOptions = filterOptions,
|
filterOptions = filterOptions,
|
||||||
|
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -560,6 +563,7 @@ fun CollectionFolderGrid(
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
useSeriesForPrimary: Boolean = true,
|
useSeriesForPrimary: Boolean = true,
|
||||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||||
|
focusRequesterOnEmpty: FocusRequester? = null,
|
||||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
viewModel: CollectionFolderViewModel =
|
viewModel: CollectionFolderViewModel =
|
||||||
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
|
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
|
||||||
|
|
@ -615,6 +619,7 @@ fun CollectionFolderGrid(
|
||||||
pager = pager,
|
pager = pager,
|
||||||
sortAndDirection = sortAndDirection!!,
|
sortAndDirection = sortAndDirection!!,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = { position, item ->
|
onLongClickItem = { position, item ->
|
||||||
moreDialog.makePresent(PositionItem(position, item))
|
moreDialog.makePresent(PositionItem(position, item))
|
||||||
|
|
@ -759,6 +764,7 @@ fun CollectionFolderGridContent(
|
||||||
currentFilter: GetItemsFilter = GetItemsFilter(),
|
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||||
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||||
|
focusRequesterOnEmpty: FocusRequester? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|
@ -767,7 +773,11 @@ fun CollectionFolderGridContent(
|
||||||
var viewOptions by remember { mutableStateOf(viewOptions) }
|
var viewOptions by remember { mutableStateOf(viewOptions) }
|
||||||
|
|
||||||
val gridFocusRequester = remember { FocusRequester() }
|
val gridFocusRequester = remember { FocusRequester() }
|
||||||
|
if (pager.isNotEmpty()) {
|
||||||
RequestOrRestoreFocus(gridFocusRequester)
|
RequestOrRestoreFocus(gridFocusRequester)
|
||||||
|
} else {
|
||||||
|
focusRequesterOnEmpty?.tryRequestFocus()
|
||||||
|
}
|
||||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
var position by rememberInt(initialPosition)
|
var position by rememberInt(initialPosition)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
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.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
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.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TabRow(
|
fun TabRow(
|
||||||
selectedTabIndex: Int,
|
selectedTabIndex: Int,
|
||||||
tabs: List<String>,
|
tabs: List<String>,
|
||||||
|
focusRequesters: List<FocusRequester>,
|
||||||
onClick: (Int) -> Unit,
|
onClick: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -55,7 +59,6 @@ fun TabRow(
|
||||||
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
|
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
|
|
||||||
var rowHasFocus by remember { mutableStateOf(false) }
|
var rowHasFocus by remember { mutableStateOf(false) }
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -68,6 +71,7 @@ fun TabRow(
|
||||||
onEnter = {
|
onEnter = {
|
||||||
// If entering from left or right, use last or first tab
|
// If entering from left or right, use last or first tab
|
||||||
// Otherwise use the selected tab
|
// Otherwise use the selected tab
|
||||||
|
Timber.v("onEnter requestedFocusDirection=$requestedFocusDirection, selectedTabIndex=$selectedTabIndex")
|
||||||
val focusRequester =
|
val focusRequester =
|
||||||
if (requestedFocusDirection == FocusDirection.Left) {
|
if (requestedFocusDirection == FocusDirection.Left) {
|
||||||
focusRequesters.lastOrNull()
|
focusRequesters.lastOrNull()
|
||||||
|
|
@ -183,6 +187,7 @@ private fun TabRowPreview() {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = 1,
|
selectedTabIndex = 1,
|
||||||
tabs = listOf("Tab 1", "Tab 2", "Tab 3"),
|
tabs = listOf("Tab 1", "Tab 2", "Tab 3"),
|
||||||
|
focusRequesters = listOf(),
|
||||||
onClick = {},
|
onClick = {},
|
||||||
)
|
)
|
||||||
Tab(
|
Tab(
|
||||||
|
|
|
||||||
|
|
@ -93,14 +93,19 @@ fun CollectionFolderLiveTv(
|
||||||
remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) }
|
remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) }
|
||||||
val folders by viewModel.recordingFolders.observeAsState(listOf())
|
val folders by viewModel.recordingFolders.observeAsState(listOf())
|
||||||
|
|
||||||
|
val tvGuideStr = stringResource(R.string.tv_guide)
|
||||||
|
val tvDvrStr = stringResource(R.string.tv_dvr_schedule)
|
||||||
val tabs =
|
val tabs =
|
||||||
|
remember(folders) {
|
||||||
listOf(
|
listOf(
|
||||||
TabId(stringResource(R.string.tv_guide), UUID.randomUUID()),
|
TabId(tvGuideStr, UUID.randomUUID()),
|
||||||
TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
|
TabId(tvDvrStr, UUID.randomUUID()),
|
||||||
) + folders
|
) + folders
|
||||||
|
}
|
||||||
|
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val tabFocusRequesters = remember(tabs.size) { List(tabs.size) { FocusRequester() } }
|
||||||
|
|
||||||
val firstTabFocusRequester = remember { FocusRequester() }
|
val firstTabFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
@ -133,6 +138,7 @@ fun CollectionFolderLiveTv(
|
||||||
.focusRequester(firstTabFocusRequester),
|
.focusRequester(firstTabFocusRequester),
|
||||||
tabs = tabs.map { it.title },
|
tabs = tabs.map { it.title },
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
focusRequesters = tabFocusRequesters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
@ -150,7 +156,9 @@ fun CollectionFolderLiveTv(
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
DvrSchedule(
|
DvrSchedule(
|
||||||
true,
|
requestFocusAfterLoading = true,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters[1],
|
||||||
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
|
|
@ -176,6 +184,7 @@ fun CollectionFolderLiveTv(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
defaultViewOptions = ViewOptions(),
|
defaultViewOptions = ViewOptions(),
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ fun CollectionFolderMovie(
|
||||||
)
|
)
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
|
||||||
|
|
||||||
val firstTabFocusRequester = remember { FocusRequester() }
|
val firstTabFocusRequester = remember { FocusRequester() }
|
||||||
// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
@ -88,6 +89,7 @@ fun CollectionFolderMovie(
|
||||||
.focusRequester(firstTabFocusRequester),
|
.focusRequester(firstTabFocusRequester),
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
focusRequesters = tabFocusRequesters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
@ -136,6 +138,7 @@ fun CollectionFolderMovie(
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
playEnabled = true,
|
playEnabled = true,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +171,7 @@ fun CollectionFolderMovie(
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ fun CollectionFolderTv(
|
||||||
)
|
)
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
|
||||||
|
|
||||||
val firstTabFocusRequester = remember { FocusRequester() }
|
val firstTabFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
@ -91,6 +92,7 @@ fun CollectionFolderTv(
|
||||||
.focusRequester(firstTabFocusRequester),
|
.focusRequester(firstTabFocusRequester),
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
focusRequesters = tabFocusRequesters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
@ -138,6 +140,7 @@ fun CollectionFolderTv(
|
||||||
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ fun FavoritesPage(
|
||||||
stringResource(R.string.playlists),
|
stringResource(R.string.playlists),
|
||||||
stringResource(R.string.people),
|
stringResource(R.string.people),
|
||||||
)
|
)
|
||||||
|
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -107,6 +108,7 @@ fun FavoritesPage(
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
|
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
focusRequesters = tabFocusRequesters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// TODO playEnabled = true for movies & episodes
|
// TODO playEnabled = true for movies & episodes
|
||||||
|
|
@ -139,6 +141,7 @@ fun FavoritesPage(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
filterOptions = DefaultForFavoritesFilterOptions,
|
filterOptions = DefaultForFavoritesFilterOptions,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,6 +173,7 @@ fun FavoritesPage(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
filterOptions = DefaultForFavoritesFilterOptions,
|
filterOptions = DefaultForFavoritesFilterOptions,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,6 +206,7 @@ fun FavoritesPage(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
filterOptions = DefaultForFavoritesFilterOptions,
|
filterOptions = DefaultForFavoritesFilterOptions,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +238,7 @@ fun FavoritesPage(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
filterOptions = DefaultForFavoritesFilterOptions,
|
filterOptions = DefaultForFavoritesFilterOptions,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,6 +270,7 @@ fun FavoritesPage(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
filterOptions = DefaultForFavoritesFilterOptions,
|
filterOptions = DefaultForFavoritesFilterOptions,
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,6 +307,7 @@ fun FavoritesPage(
|
||||||
},
|
},
|
||||||
playEnabled = false,
|
playEnabled = false,
|
||||||
filterOptions = listOf(),
|
filterOptions = listOf(),
|
||||||
|
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ class DvrScheduleViewModel
|
||||||
@Composable
|
@Composable
|
||||||
fun DvrSchedule(
|
fun DvrSchedule(
|
||||||
requestFocusAfterLoading: Boolean,
|
requestFocusAfterLoading: Boolean,
|
||||||
|
focusRequesterOnEmpty: FocusRequester,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: DvrScheduleViewModel = hiltViewModel(),
|
viewModel: DvrScheduleViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -138,7 +139,13 @@ fun DvrSchedule(
|
||||||
var showDialog by remember { mutableStateOf<BaseItem?>(null) }
|
var showDialog by remember { mutableStateOf<BaseItem?>(null) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
if (requestFocusAfterLoading) {
|
if (requestFocusAfterLoading) {
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) {
|
||||||
|
if (active.isNotEmpty() || recordings.isNotEmpty()) {
|
||||||
|
focusRequester.tryRequestFocus()
|
||||||
|
} else {
|
||||||
|
focusRequesterOnEmpty.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DvrScheduleContent(
|
DvrScheduleContent(
|
||||||
activeRecordings = active,
|
activeRecordings = active,
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,18 @@ fun SeriesOverviewContent(
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollConnection = rememberDelayedNestedScroll()
|
val scrollConnection = rememberDelayedNestedScroll()
|
||||||
var requestFocusAfterSeason by remember { mutableStateOf(false) }
|
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(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
|
|
@ -138,17 +150,13 @@ fun SeriesOverviewContent(
|
||||||
}
|
}
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
tabs =
|
tabs = tabs,
|
||||||
seasons.mapNotNull {
|
|
||||||
it?.name
|
|
||||||
?: it?.data?.indexNumber?.let { stringResource(R.string.tv_season) + " $it" }
|
|
||||||
?: ""
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedTabIndex = it
|
selectedTabIndex = it
|
||||||
onChangeSeason.invoke(it)
|
onChangeSeason.invoke(it)
|
||||||
requestFocusAfterSeason = true
|
requestFocusAfterSeason = true
|
||||||
},
|
},
|
||||||
|
focusRequesters = focusRequesters,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(tabRowFocusRequester)
|
.focusRequester(tabRowFocusRequester)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Tracks
|
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.PlaylistCreator
|
||||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
import com.github.damontecres.wholphin.services.RefreshRateService
|
||||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.onMain
|
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.TrackActivityPlaybackListener
|
||||||
import com.github.damontecres.wholphin.util.checkForSupport
|
import com.github.damontecres.wholphin.util.checkForSupport
|
||||||
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
|
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.subtitleMimeTypes
|
||||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -598,6 +601,10 @@ class PlaybackViewModel
|
||||||
source?.let { source ->
|
source?.let { source ->
|
||||||
val mediaUrl =
|
val mediaUrl =
|
||||||
if (source.supportsDirectPlay) {
|
if (source.supportsDirectPlay) {
|
||||||
|
if (source.isRemote && source.path.isNotNullOrBlank()) {
|
||||||
|
Timber.i("Playback is remote for source: %s", source.id)
|
||||||
|
source.path
|
||||||
|
} else {
|
||||||
api.videosApi.getVideoStreamUrl(
|
api.videosApi.getVideoStreamUrl(
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
mediaSourceId = source.id,
|
mediaSourceId = source.id,
|
||||||
|
|
@ -605,6 +612,7 @@ class PlaybackViewModel
|
||||||
tag = source.eTag,
|
tag = source.eTag,
|
||||||
playSessionId = response.playSessionId,
|
playSessionId = response.playSessionId,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else if (source.supportsDirectStream) {
|
} else if (source.supportsDirectStream) {
|
||||||
source.transcodingUrl?.let(api::createUrl)
|
source.transcodingUrl?.let(api::createUrl)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -657,7 +665,12 @@ class PlaybackViewModel
|
||||||
.setMediaId(itemId.toString())
|
.setMediaId(itemId.toString())
|
||||||
.setUri(mediaUrl.toUri())
|
.setUri(mediaUrl.toUri())
|
||||||
.setSubtitleConfigurations(listOfNotNull(externalSubtitle))
|
.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 =
|
val playback =
|
||||||
CurrentPlayback(
|
CurrentPlayback(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ object Codec {
|
||||||
const val `3GP` = "3gp"
|
const val `3GP` = "3gp"
|
||||||
const val ASF = "asf"
|
const val ASF = "asf"
|
||||||
const val AVI = "avi"
|
const val AVI = "avi"
|
||||||
|
const val DASH = "dash"
|
||||||
const val DVR_MS = "dvr-ms"
|
const val DVR_MS = "dvr-ms"
|
||||||
const val HLS = "hls"
|
const val HLS = "hls"
|
||||||
const val M2V = "m2v"
|
const val M2V = "m2v"
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ fun createDeviceProfile(
|
||||||
downMixAudio: Boolean,
|
downMixAudio: Boolean,
|
||||||
assDirectPlay: Boolean,
|
assDirectPlay: Boolean,
|
||||||
pgsDirectPlay: Boolean,
|
pgsDirectPlay: Boolean,
|
||||||
|
dolbyVisionELDirectPlay: Boolean,
|
||||||
jellyfinTenEleven: Boolean,
|
jellyfinTenEleven: Boolean,
|
||||||
) = buildDeviceProfile {
|
) = buildDeviceProfile {
|
||||||
val allowedAudioCodecs =
|
val allowedAudioCodecs =
|
||||||
|
|
@ -156,6 +157,7 @@ fun createDeviceProfile(
|
||||||
|
|
||||||
container(
|
container(
|
||||||
Codec.Container.ASF,
|
Codec.Container.ASF,
|
||||||
|
Codec.Container.DASH,
|
||||||
Codec.Container.HLS,
|
Codec.Container.HLS,
|
||||||
Codec.Container.M4V,
|
Codec.Container.M4V,
|
||||||
Codec.Container.MKV,
|
Codec.Container.MKV,
|
||||||
|
|
@ -427,10 +429,12 @@ fun createDeviceProfile(
|
||||||
if (jellyfinTenEleven) add("DOVIInvalid")
|
if (jellyfinTenEleven) add("DOVIInvalid")
|
||||||
|
|
||||||
if (!supportsHevcDolbyVisionEL) {
|
if (!supportsHevcDolbyVisionEL) {
|
||||||
|
if (!dolbyVisionELDirectPlay) {
|
||||||
if (jellyfinTenEleven) {
|
if (jellyfinTenEleven) {
|
||||||
add("DOVIWithEL")
|
add("DOVIWithEL")
|
||||||
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus")
|
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!supportsHevcDolbyVision) {
|
if (!supportsHevcDolbyVision) {
|
||||||
add(VideoRangeType.DOVI.serialName)
|
add(VideoRangeType.DOVI.serialName)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ message PlaybackOverrides{
|
||||||
bool direct_play_ass = 3;
|
bool direct_play_ass = 3;
|
||||||
bool direct_play_pgs = 4;
|
bool direct_play_pgs = 4;
|
||||||
MediaExtensionStatus media_extensions_enabled = 5;
|
MediaExtensionStatus media_extensions_enabled = 5;
|
||||||
|
bool direct_play_dolby_vision_e_l = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PlaybackPreferences {
|
message PlaybackPreferences {
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,8 @@
|
||||||
<string name="dolby_digital">DD</string>
|
<string name="dolby_digital">DD</string>
|
||||||
<string name="dolby_digital_plus">DD+</string>
|
<string name="dolby_digital_plus">DD+</string>
|
||||||
<string name="dts">DTS</string>
|
<string name="dts">DTS</string>
|
||||||
|
<string name="force_dovi_profile_7">Direct play Dolby Vision Profile 7</string>
|
||||||
|
<string name="force_dovi_profile_7_summary">Ignores device compatibility checks</string>
|
||||||
|
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
<string name="request">Request</string>
|
<string name="request">Request</string>
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasourc
|
||||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
|
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-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-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 = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
|
||||||
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" }
|
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue