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 3541633d..0abe6cba 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 @@ -655,6 +655,18 @@ sealed interface AppPreference { 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( 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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt index bd13d25d..4a453cce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt @@ -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)) + } + } +} 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 new file mode 100644 index 00000000..dafb5ed9 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt @@ -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, + 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), + ) + } + } +} 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 76794b73..66dd2779 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 @@ -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) { 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 8fddd0bc..7cef5122 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 @@ -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) { 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 8b63fe27..8155fbc5 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 @@ -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) { 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 3546c8de..002ca4ef 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 @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index aaab1b9c..bd04b4c6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -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), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index 534fe23e..1bf0869a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -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), - ) - } else { - Spacer(Modifier.height(32.dp)) - } - } - + SimpleStarRating( + dto.communityRating, + Modifier.height(20.dp), + ) dto.taglines?.firstOrNull()?.let { tagline -> Text( text = tagline, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt index 7c1425a4..d4cacc99 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -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), - ) - } else { - Spacer(Modifier.height(24.dp)) - } - } ?: Spacer(Modifier.height(24.dp)) + SimpleStarRating( + dto.communityRating, + Modifier.height(20.dp), + ) } OverviewText( overview = dto.overview ?: "", diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index a71d9423..6186ff80 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -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( 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 6c19f62f..f2fde9f3 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 @@ -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,62 +132,22 @@ fun SeriesOverviewContent( PaddingValues(start = 16.dp, end = 16.dp) } TabRow( - selectedTabIndex = focusTabIndex, + selectedTabIndex = selectedTabIndex, + tabs = + seasons.mapNotNull { + it?.name + ?: (stringResource(R.string.tv_season) + " ${it?.data?.indexNumber}") + }, + onClick = { + selectedTabIndex = it + onFocus.invoke(SeriesOverviewPosition(it, 0)) + }, modifier = Modifier - .ifElse( - focusRequesters.size > selectedTabIndex, - { Modifier.focusRestorer(focusRequesters[selectedTabIndex]) }, - ).focusRequester(tabRowFocusRequester) + .focusRequester(tabRowFocusRequester) .padding(paddingValues) - .fillMaxWidth() - .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, - ) - } - }, - ) { - seasons.forEachIndexed { index, season -> - season?.let { season -> - Tab( - selected = focusTabIndex == index, - onFocus = { focusTabIndex = index }, - onClick = { - selectedTabIndex = index - onFocus.invoke(SeriesOverviewPosition(index, 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), - ) - } - } - } - } + .fillMaxWidth(), + ) } item { series.name?.let { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index 806ab9f0..809387be 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -90,7 +90,7 @@ class HomeViewModel if (resume.isNotEmpty()) { add( HomeRow( - titleRes = R.string.recently_added, + titleRes = R.string.continue_watching, items = resume, ), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt index 7420b038..65e1d57e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt @@ -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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index c88245fc..cb488345 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -208,6 +208,7 @@ fun PlaybackPage( updateSkipIndicator = updateSkipIndicator, skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume, onInteraction = viewModel::reportInteraction, + oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause, ) val showSegment = diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index e0db13ab..f08a9ff2 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -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{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c94342d7..c575edf8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,6 +142,8 @@ Aired Episode Order Create new playlist Add to playlist + Pause with one click + Press D-Pad center to pause/play Playback Backend MPV: Use hardware decoding Disable if you experience crashes