mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge remote-tracking branch 'origin/main' into develop/mpv
This commit is contained in:
commit
9a093afd38
17 changed files with 326 additions and 341 deletions
|
|
@ -655,6 +655,18 @@ sealed interface AppPreference<T> {
|
||||||
summaryOff = R.string.disabled,
|
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 =
|
val PlayerBackendPref =
|
||||||
AppChoicePreference<PlayerBackend>(
|
AppChoicePreference<PlayerBackend>(
|
||||||
title = R.string.player_backend,
|
title = R.string.player_backend,
|
||||||
|
|
@ -756,6 +768,7 @@ val advancedPreferences =
|
||||||
title = R.string.playback,
|
title = R.string.playback,
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
|
AppPreference.OneClickPause,
|
||||||
AppPreference.SkipIntros,
|
AppPreference.SkipIntros,
|
||||||
AppPreference.SkipOutros,
|
AppPreference.SkipOutros,
|
||||||
AppPreference.SkipCommercials,
|
AppPreference.SkipCommercials,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.selection.selectable
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
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.playOnClickSound
|
||||||
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||||
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
enum class StarRatingPrecision {
|
enum class StarRatingPrecision {
|
||||||
FULL,
|
FULL,
|
||||||
|
|
@ -63,6 +71,33 @@ val EmptyStarColor = Color(0x2AFFC700)
|
||||||
|
|
||||||
val ratingBarHeight: Dp = 32.dp
|
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
|
* 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,20 +20,12 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
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.data.VideoSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
|
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
|
||||||
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
|
|
@ -103,7 +95,6 @@ fun CollectionFolderLiveTv(
|
||||||
TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
|
TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
|
||||||
) + folders
|
) + folders
|
||||||
|
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -129,55 +120,13 @@ fun CollectionFolderLiveTv(
|
||||||
exit = slideOutVertically() + fadeOut(),
|
exit = slideOutVertically() + fadeOut(),
|
||||||
) {
|
) {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = focusTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
||||||
.focusRestorer(firstTabFocusRequester)
|
.focusRequester(firstTabFocusRequester),
|
||||||
.onFocusChanged {
|
tabs = tabs.map { it.title },
|
||||||
if (!it.isFocused) {
|
onClick = { selectedTabIndex = it },
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,9 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
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.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.RecommendedMovie
|
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.MovieSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
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.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -63,7 +55,6 @@ fun CollectionFolderMovie(
|
||||||
stringResource(R.string.collections),
|
stringResource(R.string.collections),
|
||||||
stringResource(R.string.genres),
|
stringResource(R.string.genres),
|
||||||
)
|
)
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -90,55 +81,13 @@ fun CollectionFolderMovie(
|
||||||
exit = slideOutVertically() + fadeOut(),
|
exit = slideOutVertically() + fadeOut(),
|
||||||
) {
|
) {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = focusTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
||||||
.focusRestorer(firstTabFocusRequester)
|
.focusRequester(firstTabFocusRequester),
|
||||||
.onFocusChanged {
|
tabs = tabs,
|
||||||
if (!it.isFocused) {
|
onClick = { selectedTabIndex = it },
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,9 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
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.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
|
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.data.SeriesSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -61,7 +53,6 @@ fun CollectionFolderTv(
|
||||||
stringResource(R.string.library),
|
stringResource(R.string.library),
|
||||||
stringResource(R.string.genres),
|
stringResource(R.string.genres),
|
||||||
)
|
)
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -88,55 +79,13 @@ fun CollectionFolderTv(
|
||||||
exit = slideOutVertically() + fadeOut(),
|
exit = slideOutVertically() + fadeOut(),
|
||||||
) {
|
) {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = focusTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
||||||
.focusRestorer(firstTabFocusRequester)
|
.focusRequester(firstTabFocusRequester),
|
||||||
.onFocusChanged {
|
tabs = tabs,
|
||||||
if (!it.isFocused) {
|
onClick = { selectedTabIndex = it },
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
|
||||||
|
|
@ -19,23 +19,16 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
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.EpisodeSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||||
|
|
@ -66,14 +59,9 @@ fun FavoritesPage(
|
||||||
stringResource(R.string.tv_shows),
|
stringResource(R.string.tv_shows),
|
||||||
stringResource(R.string.episodes),
|
stringResource(R.string.episodes),
|
||||||
)
|
)
|
||||||
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
|
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
tabFocusRequesters.getOrNull(selectedTabIndex)?.tryRequestFocus()
|
|
||||||
}
|
|
||||||
LaunchedEffect(selectedTabIndex) {
|
LaunchedEffect(selectedTabIndex) {
|
||||||
preferencesViewModel.saveRememberedTab(
|
preferencesViewModel.saveRememberedTab(
|
||||||
preferences,
|
preferences,
|
||||||
|
|
@ -97,52 +85,12 @@ fun FavoritesPage(
|
||||||
exit = slideOutVertically() + fadeOut(),
|
exit = slideOutVertically() + fadeOut(),
|
||||||
) {
|
) {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = focusTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
|
||||||
.focusRestorer(tabFocusRequesters[0])
|
tabs = tabs,
|
||||||
.onFocusChanged {
|
onClick = { selectedTabIndex = it },
|
||||||
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]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -17,6 +21,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
|
@ -322,9 +327,11 @@ fun PersonHeader(
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
Row(
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||||
modifier = modifier,
|
modifier = modifier.bringIntoViewRequester(bringIntoViewRequester),
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
|
|
@ -414,10 +421,18 @@ fun PersonHeader(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
if (focused) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
OverviewText(
|
OverviewText(
|
||||||
overview = overview ?: "",
|
overview = overview ?: "",
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
onClick = overviewOnClick,
|
onClick = overviewOnClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.StarRating
|
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
||||||
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
|
|
||||||
import com.github.damontecres.wholphin.ui.components.TitleValueText
|
import com.github.damontecres.wholphin.ui.components.TitleValueText
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
|
@ -102,24 +100,13 @@ fun MovieDetailsHeader(
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = details,
|
texts = details,
|
||||||
textStyle = MaterialTheme.typography.titleMedium,
|
textStyle = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
dto.communityRating?.let {
|
SimpleStarRating(
|
||||||
if (it > 0f) {
|
dto.communityRating,
|
||||||
StarRating(
|
Modifier.height(20.dp),
|
||||||
rating100 = (it * 10).toInt(),
|
|
||||||
onRatingChange = {},
|
|
||||||
enabled = false,
|
|
||||||
precision = StarRatingPrecision.HALF,
|
|
||||||
playSoundOnFocus = true,
|
|
||||||
modifier = Modifier.height(32.dp),
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Spacer(Modifier.height(32.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.taglines?.firstOrNull()?.let { tagline ->
|
dto.taglines?.firstOrNull()?.let { tagline ->
|
||||||
Text(
|
Text(
|
||||||
text = tagline,
|
text = tagline,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.ui.detail.series
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
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.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.StarRating
|
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
||||||
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
|
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
import com.github.damontecres.wholphin.util.formatDateTime
|
import com.github.damontecres.wholphin.util.formatDateTime
|
||||||
|
|
@ -69,20 +67,10 @@ fun FocusedEpisodeHeader(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
dto.communityRating?.let {
|
SimpleStarRating(
|
||||||
if (it > 0f) {
|
dto.communityRating,
|
||||||
StarRating(
|
Modifier.height(20.dp),
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
OverviewText(
|
OverviewText(
|
||||||
overview = dto.overview ?: "",
|
overview = dto.overview ?: "",
|
||||||
|
|
|
||||||
|
|
@ -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.ExpandablePlayButton
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.StarRating
|
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
||||||
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
|
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
|
@ -467,18 +466,10 @@ fun SeriesDetailsHeader(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.communityRating?.let {
|
SimpleStarRating(
|
||||||
if (it >= 0f) {
|
dto.communityRating,
|
||||||
StarRating(
|
Modifier.height(20.dp),
|
||||||
rating100 = (it * 10).toInt(),
|
|
||||||
onRatingChange = {},
|
|
||||||
enabled = false,
|
|
||||||
precision = StarRatingPrecision.HALF,
|
|
||||||
playSoundOnFocus = true,
|
|
||||||
modifier = Modifier.height(24.dp),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.overview?.let { overview ->
|
dto.overview?.let { overview ->
|
||||||
OverviewText(
|
OverviewText(
|
||||||
|
|
|
||||||
|
|
@ -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.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
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 androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.wholphin.R
|
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.cards.BannerCard
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
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.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -80,10 +76,7 @@ fun SeriesOverviewContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
var focusTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
|
|
||||||
var selectedTabIndex 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 tabRowFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val focusedEpisode =
|
val focusedEpisode =
|
||||||
|
|
@ -139,63 +132,23 @@ fun SeriesOverviewContent(
|
||||||
PaddingValues(start = 16.dp, end = 16.dp)
|
PaddingValues(start = 16.dp, end = 16.dp)
|
||||||
}
|
}
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = focusTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
tabs =
|
||||||
Modifier
|
seasons.mapNotNull {
|
||||||
.ifElse(
|
it?.name
|
||||||
focusRequesters.size > selectedTabIndex,
|
?: (stringResource(R.string.tv_season) + " ${it?.data?.indexNumber}")
|
||||||
{ Modifier.focusRestorer(focusRequesters[selectedTabIndex]) },
|
|
||||||
).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 = {
|
onClick = {
|
||||||
selectedTabIndex = index
|
selectedTabIndex = it
|
||||||
onFocus.invoke(SeriesOverviewPosition(index, 0))
|
onFocus.invoke(SeriesOverviewPosition(it, 0))
|
||||||
},
|
},
|
||||||
colors =
|
|
||||||
TabDefaults.pillIndicatorTabColors(
|
|
||||||
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(focusRequesters[index]),
|
.focusRequester(tabRowFocusRequester)
|
||||||
) {
|
.padding(paddingValues)
|
||||||
Text(
|
.fillMaxWidth(),
|
||||||
text =
|
|
||||||
season.name
|
|
||||||
?: (stringResource(R.string.tv_season) + " ${season.data.indexNumber}"),
|
|
||||||
modifier = Modifier.padding(8.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
series.name?.let {
|
series.name?.let {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class HomeViewModel
|
||||||
if (resume.isNotEmpty()) {
|
if (resume.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
HomeRow(
|
HomeRow(
|
||||||
titleRes = R.string.recently_added,
|
titleRes = R.string.continue_watching,
|
||||||
items = resume,
|
items = resume,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class PlaybackKeyHandler(
|
||||||
private val controllerViewState: ControllerViewState,
|
private val controllerViewState: ControllerViewState,
|
||||||
private val updateSkipIndicator: (Long) -> Unit,
|
private val updateSkipIndicator: (Long) -> Unit,
|
||||||
private val skipBackOnResume: Duration?,
|
private val skipBackOnResume: Duration?,
|
||||||
|
private val oneClickPause: Boolean,
|
||||||
private val onInteraction: () -> Unit,
|
private val onInteraction: () -> Unit,
|
||||||
) {
|
) {
|
||||||
fun onKeyEvent(it: KeyEvent): Boolean {
|
fun onKeyEvent(it: KeyEvent): Boolean {
|
||||||
|
|
@ -33,7 +34,7 @@ class PlaybackKeyHandler(
|
||||||
result = false
|
result = false
|
||||||
} else if (it.type != KeyEventType.KeyUp) {
|
} else if (it.type != KeyEventType.KeyUp) {
|
||||||
result = false
|
result = false
|
||||||
} else if (isDpad(it)) {
|
} else if (isDirectionalDpad(it) || isEnterKey(it)) {
|
||||||
if (!controllerViewState.controlsVisible) {
|
if (!controllerViewState.controlsVisible) {
|
||||||
if (skipWithLeftRight && it.key == Key.DirectionLeft) {
|
if (skipWithLeftRight && it.key == Key.DirectionLeft) {
|
||||||
updateSkipIndicator(-seekBack.inWholeMilliseconds)
|
updateSkipIndicator(-seekBack.inWholeMilliseconds)
|
||||||
|
|
@ -41,6 +42,15 @@ class PlaybackKeyHandler(
|
||||||
} else if (skipWithLeftRight && it.key == Key.DirectionRight) {
|
} else if (skipWithLeftRight && it.key == Key.DirectionRight) {
|
||||||
player.seekForward(seekForward)
|
player.seekForward(seekForward)
|
||||||
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
||||||
|
} else if (oneClickPause && isEnterKey(it)) {
|
||||||
|
Util.handlePlayPauseButtonAction(player)
|
||||||
|
if (!player.isPlaying) {
|
||||||
|
controllerViewState.showControls()
|
||||||
|
} else {
|
||||||
|
skipBackOnResume?.let {
|
||||||
|
player.seekBack(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
controllerViewState.showControls()
|
controllerViewState.showControls()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,7 @@ fun PlaybackPage(
|
||||||
updateSkipIndicator = updateSkipIndicator,
|
updateSkipIndicator = updateSkipIndicator,
|
||||||
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
||||||
onInteraction = viewModel::reportInteraction,
|
onInteraction = viewModel::reportInteraction,
|
||||||
|
oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause,
|
||||||
)
|
)
|
||||||
|
|
||||||
val showSegment =
|
val showSegment =
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,9 @@ message PlaybackPreferences {
|
||||||
int64 pass_out_protection_ms = 16;
|
int64 pass_out_protection_ms = 16;
|
||||||
PrefContentScale global_content_scale = 17;
|
PrefContentScale global_content_scale = 17;
|
||||||
ShowNextUpWhen show_next_up_when = 18;
|
ShowNextUpWhen show_next_up_when = 18;
|
||||||
PlayerBackend player_backend = 19;
|
bool one_click_pause = 19;
|
||||||
MpvOptions mpv_options = 20;
|
PlayerBackend player_backend = 20;
|
||||||
|
MpvOptions mpv_options = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomePagePreferences{
|
message HomePagePreferences{
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@
|
||||||
<string name="aired_episode_order">Aired Episode Order</string>
|
<string name="aired_episode_order">Aired Episode Order</string>
|
||||||
<string name="create_playlist">Create new playlist</string>
|
<string name="create_playlist">Create new playlist</string>
|
||||||
<string name="add_to_playlist">Add to 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="player_backend">Playback Backend</string>
|
||||||
<string name="mpv_hardware_decoding">MPV: Use hardware decoding</string>
|
<string name="mpv_hardware_decoding">MPV: Use hardware decoding</string>
|
||||||
<string name="disable_if_crash">Disable if you experience crashes</string>
|
<string name="disable_if_crash">Disable if you experience crashes</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue