Merge remote-tracking branch 'origin/main' into develop/mpv

This commit is contained in:
Damontecres 2025-11-05 16:47:35 -05:00
commit 9a093afd38
No known key found for this signature in database
17 changed files with 326 additions and 341 deletions

View file

@ -655,6 +655,18 @@ sealed interface AppPreference<T> {
summaryOff = R.string.disabled,
)
val OneClickPause =
AppSwitchPreference(
title = R.string.one_click_pause,
defaultValue = false,
getter = { it.playbackPreferences.oneClickPause },
setter = { prefs, value ->
prefs.updatePlaybackPreferences { oneClickPause = value }
},
summaryOn = R.string.one_click_pause_summary_on,
summaryOff = R.string.disabled,
)
val PlayerBackendPref =
AppChoicePreference<PlayerBackend>(
title = R.string.player_backend,
@ -756,6 +768,7 @@ val advancedPreferences =
title = R.string.playback,
preferences =
listOf(
AppPreference.OneClickPause,
AppPreference.SkipIntros,
AppPreference.SkipOutros,
AppPreference.SkipCommercials,

View file

@ -4,8 +4,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.selection.selectable
@ -33,12 +36,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import java.util.Locale
enum class StarRatingPrecision {
FULL,
@ -63,6 +71,33 @@ val EmptyStarColor = Color(0x2AFFC700)
val ratingBarHeight: Dp = 32.dp
@Composable
fun SimpleStarRating(
communityRating: Float?,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
communityRating?.let {
Text(
text = String.format(Locale.getDefault(), "%.1f", it),
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier,
)
Icon(
imageVector = Icons.Filled.Star,
tint = FilledStarColor,
contentDescription = null,
modifier = Modifier,
)
}
}
}
/**
* Shows a rating out of 5 stars
*/
@ -211,3 +246,15 @@ fun StarRating(
}
}
}
@PreviewTvSpec
@Composable
private fun SimpleStarRatingPreview() {
WholphinTheme {
Column {
SimpleStarRating(7.5f, Modifier.height(32.dp))
SimpleStarRating(7.5f, Modifier.height(20.dp))
SimpleStarRating(null, Modifier.height(32.dp))
}
}
}

View file

@ -0,0 +1,182 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
@Composable
fun TabRow(
selectedTabIndex: Int,
tabs: List<String>,
onClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val state = rememberLazyListState()
LaunchedEffect(Unit) {
state.animateScrollToItem(selectedTabIndex)
}
val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
var rowHasFocus by remember { mutableStateOf(false) }
LazyRow(
state = state,
modifier =
modifier
.onFocusChanged {
rowHasFocus = it.hasFocus
}.focusProperties {
onEnter = {
focusRequesters.getOrNull(selectedTabIndex)?.tryRequestFocus()
}
},
) {
itemsIndexed(tabs) { index, tabTitle ->
val interactionSource = remember { MutableInteractionSource() }
Tab(
title = tabTitle,
selected = index == selectedTabIndex,
rowActive = rowHasFocus,
interactionSource = interactionSource,
onClick = {
onClick.invoke(index)
},
modifier = Modifier.focusRequester(focusRequesters[index]),
)
}
}
}
@Composable
fun Tab(
title: String,
selected: Boolean,
rowActive: Boolean,
onClick: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
) {
var tabWidth by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
val focused by interactionSource.collectIsFocusedAsState()
val contentColor =
if (rowActive || selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = .5f)
}
Box(
modifier =
modifier
.clickable(
enabled = true,
interactionSource = interactionSource,
onClick = onClick,
indication = null,
).onGloballyPositioned {
tabWidth = with(density) { it.size.width.toDp() }
},
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier,
) {
Text(
text = title,
fontSize = 16.sp,
color = contentColor,
modifier = Modifier.padding(horizontal = 16.dp),
)
TabIndicator(
selected = selected,
rowActive = rowActive,
focused = focused,
tabWidth = tabWidth,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}
@Composable
fun TabIndicator(
selected: Boolean,
rowActive: Boolean,
focused: Boolean,
tabWidth: Dp,
modifier: Modifier = Modifier,
) {
val width by animateDpAsState(if (rowActive && focused) tabWidth else tabWidth * .25f)
val backgroundColor =
if (rowActive && focused) {
MaterialTheme.colorScheme.border
} else if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
Color.Transparent
}
Box(
modifier =
modifier
.height(2.dp)
.fillMaxWidth()
.width(width)
.background(backgroundColor),
)
}
@PreviewTvSpec
@Composable
private fun TabRowPreview() {
WholphinTheme {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
TabRow(
selectedTabIndex = 1,
tabs = listOf("Tab 1", "Tab 2", "Tab 3"),
onClick = {},
)
Tab(
title = "This is a Tab",
selected = true,
rowActive = true,
onClick = {},
interactionSource = remember { MutableInteractionSource() },
modifier = Modifier.width(120.dp),
)
}
}
}

View file

@ -20,20 +20,12 @@ 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.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
@ -41,10 +33,10 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
@ -103,7 +95,6 @@ fun CollectionFolderLiveTv(
TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
) + folders
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
@ -129,55 +120,13 @@ fun CollectionFolderLiveTv(
exit = slideOutVertically() + fadeOut(),
) {
TabRow(
selectedTabIndex = focusTabIndex,
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(firstTabFocusRequester)
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
tabs = {
tabs.forEachIndexed { index, tabId ->
Tab(
selected = focusTabIndex == index,
onClick = { selectedTabIndex = index },
onFocus = { focusTabIndex = index },
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
) {
Text(
text = tabId.title,
style = MaterialTheme.typography.titleMedium,
modifier =
Modifier
.padding(8.dp)
.ifElse(
index == selectedTabIndex,
Modifier.focusRequester(firstTabFocusRequester),
),
)
}
}
},
.focusRequester(firstTabFocusRequester),
tabs = tabs.map { it.title },
onClick = { selectedTabIndex = it },
)
}
when (selectedTabIndex) {

View file

@ -19,17 +19,9 @@ 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.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
@ -38,9 +30,9 @@ import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
import com.github.damontecres.wholphin.ui.components.RecommendedMovie
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -63,7 +55,6 @@ fun CollectionFolderMovie(
stringResource(R.string.collections),
stringResource(R.string.genres),
)
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
@ -90,55 +81,13 @@ fun CollectionFolderMovie(
exit = slideOutVertically() + fadeOut(),
) {
TabRow(
selectedTabIndex = focusTabIndex,
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(firstTabFocusRequester)
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
tabs = {
tabs.forEachIndexed { index, title ->
Tab(
selected = focusTabIndex == index,
onClick = { selectedTabIndex = index },
onFocus = { focusTabIndex = index },
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier =
Modifier
.padding(8.dp)
.ifElse(
index == selectedTabIndex,
Modifier.focusRequester(firstTabFocusRequester),
),
)
}
}
},
.focusRequester(firstTabFocusRequester),
tabs = tabs,
onClick = { selectedTabIndex = it },
)
}
when (selectedTabIndex) {

View file

@ -19,17 +19,9 @@ 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.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
@ -38,8 +30,8 @@ import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -61,7 +53,6 @@ fun CollectionFolderTv(
stringResource(R.string.library),
stringResource(R.string.genres),
)
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
@ -88,55 +79,13 @@ fun CollectionFolderTv(
exit = slideOutVertically() + fadeOut(),
) {
TabRow(
selectedTabIndex = focusTabIndex,
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(firstTabFocusRequester)
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
tabs = {
tabs.forEachIndexed { index, title ->
Tab(
selected = focusTabIndex == index,
onClick = { selectedTabIndex = index },
onFocus = { focusTabIndex = index },
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier =
Modifier
.padding(8.dp)
.ifElse(
index == selectedTabIndex,
Modifier.focusRequester(firstTabFocusRequester),
),
)
}
}
},
.focusRequester(firstTabFocusRequester),
tabs = tabs,
onClick = { selectedTabIndex = it },
)
}
when (selectedTabIndex) {

View file

@ -19,23 +19,16 @@ 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.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
@ -66,14 +59,9 @@ fun FavoritesPage(
stringResource(R.string.tv_shows),
stringResource(R.string.episodes),
)
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
tabFocusRequesters.getOrNull(selectedTabIndex)?.tryRequestFocus()
}
LaunchedEffect(selectedTabIndex) {
preferencesViewModel.saveRememberedTab(
preferences,
@ -97,52 +85,12 @@ fun FavoritesPage(
exit = slideOutVertically() + fadeOut(),
) {
TabRow(
selectedTabIndex = focusTabIndex,
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(tabFocusRequesters[0])
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
tabs = {
tabs.forEachIndexed { index, title ->
Tab(
selected = focusTabIndex == index,
onClick = { selectedTabIndex = index },
onFocus = { focusTabIndex = index },
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier =
Modifier
.padding(8.dp)
.focusRequester(tabFocusRequesters[index]),
)
}
}
},
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
tabs = tabs,
onClick = { selectedTabIndex = it },
)
}
when (selectedTabIndex) {

View file

@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.ui.detail
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -9,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -17,6 +21,7 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
@ -322,9 +327,11 @@ fun PersonHeader(
overviewOnClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val bringIntoViewRequester = remember { BringIntoViewRequester() }
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(32.dp),
modifier = modifier,
modifier = modifier.bringIntoViewRequester(bringIntoViewRequester),
) {
AsyncImage(
model = imageUrl,
@ -414,10 +421,18 @@ fun PersonHeader(
overflow = TextOverflow.Ellipsis,
)
}
val interactionSource = remember { MutableInteractionSource() }
val focused by interactionSource.collectIsFocusedAsState()
if (focused) {
LaunchedEffect(Unit) {
bringIntoViewRequester.bringIntoView()
}
}
OverviewText(
overview = overview ?: "",
maxLines = 3,
onClick = overviewOnClick,
interactionSource = interactionSource,
modifier = Modifier.padding(top = 8.dp),
)
}

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -34,8 +33,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.StarRating
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.components.TitleValueText
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.roundMinutes
@ -102,24 +100,13 @@ fun MovieDetailsHeader(
DotSeparatedRow(
texts = details,
textStyle = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp),
modifier = Modifier,
)
}
dto.communityRating?.let {
if (it > 0f) {
StarRating(
rating100 = (it * 10).toInt(),
onRatingChange = {},
enabled = false,
precision = StarRatingPrecision.HALF,
playSoundOnFocus = true,
modifier = Modifier.height(32.dp),
SimpleStarRating(
dto.communityRating,
Modifier.height(20.dp),
)
} else {
Spacer(Modifier.height(32.dp))
}
}
dto.taglines?.firstOrNull()?.let { tagline ->
Text(
text = tagline,

View file

@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.ui.detail.series
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -16,8 +15,7 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.StarRating
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.timeRemaining
import com.github.damontecres.wholphin.util.formatDateTime
@ -69,20 +67,10 @@ fun FocusedEpisodeHeader(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
dto.communityRating?.let {
if (it > 0f) {
StarRating(
rating100 = (it * 10).toInt(),
onRatingChange = {},
enabled = false,
precision = StarRatingPrecision.HALF,
playSoundOnFocus = true,
modifier = Modifier.height(24.dp),
SimpleStarRating(
dto.communityRating,
Modifier.height(20.dp),
)
} else {
Spacer(Modifier.height(24.dp))
}
} ?: Spacer(Modifier.height(24.dp))
}
OverviewText(
overview = dto.overview ?: "",

View file

@ -61,8 +61,7 @@ import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.StarRating
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
@ -467,18 +466,10 @@ fun SeriesDetailsHeader(
)
}
dto.communityRating?.let {
if (it >= 0f) {
StarRating(
rating100 = (it * 10).toInt(),
onRatingChange = {},
enabled = false,
precision = StarRatingPrecision.HALF,
playSoundOnFocus = true,
modifier = Modifier.height(24.dp),
SimpleStarRating(
dto.communityRating,
Modifier.height(20.dp),
)
}
}
dto.overview?.let { overview ->
OverviewText(

View file

@ -29,7 +29,6 @@ import androidx.compose.ui.draw.drawWithContent
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.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
@ -37,10 +36,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
@ -52,6 +47,7 @@ import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -80,10 +76,7 @@ fun SeriesOverviewContent(
modifier: Modifier = Modifier,
) {
val bringIntoViewRequester = remember { BringIntoViewRequester() }
var focusTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
var selectedTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
val focusRequesters = remember(seasons.size) { List(seasons.size) { FocusRequester() } }
var resolvedTabIndex by remember { mutableIntStateOf(selectedTabIndex) }
val tabRowFocusRequester = remember { FocusRequester() }
val focusedEpisode =
@ -139,63 +132,23 @@ fun SeriesOverviewContent(
PaddingValues(start = 16.dp, end = 16.dp)
}
TabRow(
selectedTabIndex = focusTabIndex,
modifier =
Modifier
.ifElse(
focusRequesters.size > selectedTabIndex,
{ Modifier.focusRestorer(focusRequesters[selectedTabIndex]) },
).focusRequester(tabRowFocusRequester)
.padding(paddingValues)
.fillMaxWidth()
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
selectedTabIndex = selectedTabIndex,
tabs =
seasons.mapNotNull {
it?.name
?: (stringResource(R.string.tv_season) + " ${it?.data?.indexNumber}")
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
) {
seasons.forEachIndexed { index, season ->
season?.let { season ->
Tab(
selected = focusTabIndex == index,
onFocus = { focusTabIndex = index },
onClick = {
selectedTabIndex = index
onFocus.invoke(SeriesOverviewPosition(index, 0))
selectedTabIndex = it
onFocus.invoke(SeriesOverviewPosition(it, 0))
},
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
modifier =
Modifier
.focusRequester(focusRequesters[index]),
) {
Text(
text =
season.name
?: (stringResource(R.string.tv_season) + " ${season.data.indexNumber}"),
modifier = Modifier.padding(8.dp),
.focusRequester(tabRowFocusRequester)
.padding(paddingValues)
.fillMaxWidth(),
)
}
}
}
}
}
item {
series.name?.let {
Text(

View file

@ -90,7 +90,7 @@ class HomeViewModel
if (resume.isNotEmpty()) {
add(
HomeRow(
titleRes = R.string.recently_added,
titleRes = R.string.continue_watching,
items = resume,
),
)

View file

@ -23,6 +23,7 @@ class PlaybackKeyHandler(
private val controllerViewState: ControllerViewState,
private val updateSkipIndicator: (Long) -> Unit,
private val skipBackOnResume: Duration?,
private val oneClickPause: Boolean,
private val onInteraction: () -> Unit,
) {
fun onKeyEvent(it: KeyEvent): Boolean {
@ -33,7 +34,7 @@ class PlaybackKeyHandler(
result = false
} else if (it.type != KeyEventType.KeyUp) {
result = false
} else if (isDpad(it)) {
} else if (isDirectionalDpad(it) || isEnterKey(it)) {
if (!controllerViewState.controlsVisible) {
if (skipWithLeftRight && it.key == Key.DirectionLeft) {
updateSkipIndicator(-seekBack.inWholeMilliseconds)
@ -41,6 +42,15 @@ class PlaybackKeyHandler(
} else if (skipWithLeftRight && it.key == Key.DirectionRight) {
player.seekForward(seekForward)
updateSkipIndicator(seekForward.inWholeMilliseconds)
} else if (oneClickPause && isEnterKey(it)) {
Util.handlePlayPauseButtonAction(player)
if (!player.isPlaying) {
controllerViewState.showControls()
} else {
skipBackOnResume?.let {
player.seekBack(it)
}
}
} else {
controllerViewState.showControls()
}

View file

@ -208,6 +208,7 @@ fun PlaybackPage(
updateSkipIndicator = updateSkipIndicator,
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
onInteraction = viewModel::reportInteraction,
oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause,
)
val showSegment =

View file

@ -67,8 +67,9 @@ message PlaybackPreferences {
int64 pass_out_protection_ms = 16;
PrefContentScale global_content_scale = 17;
ShowNextUpWhen show_next_up_when = 18;
PlayerBackend player_backend = 19;
MpvOptions mpv_options = 20;
bool one_click_pause = 19;
PlayerBackend player_backend = 20;
MpvOptions mpv_options = 21;
}
message HomePagePreferences{

View file

@ -142,6 +142,8 @@
<string name="aired_episode_order">Aired Episode Order</string>
<string name="create_playlist">Create new playlist</string>
<string name="add_to_playlist">Add to playlist</string>
<string name="one_click_pause">Pause with one click</string>
<string name="one_click_pause_summary_on">Press D-Pad center to pause/play</string>
<string name="player_backend">Playback Backend</string>
<string name="mpv_hardware_decoding">MPV: Use hardware decoding</string>
<string name="disable_if_crash">Disable if you experience crashes</string>