Resize & reshape buttons (#380)

Adds a new button implementation which allows for icon buttons to be
circular. Overall the buttons are reduced in size.

Also, the playback controls are are reduced in size, buttons are smaller
and vertical padding is reduced.

![buttons
Large](https://github.com/user-attachments/assets/2211d00f-60a7-4a61-be51-a09046fc7433)
This commit is contained in:
damontecres 2025-12-06 15:35:17 -05:00 committed by GitHub
parent ac10517028
commit 793f9802e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 492 additions and 206 deletions

View file

@ -1,7 +1,6 @@
package com.github.damontecres.wholphin.ui package com.github.damontecres.wholphin.ui
import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
@ -66,14 +65,6 @@ val SlimItemFields =
ItemFields.SORT_NAME, ItemFields.SORT_NAME,
) )
val DefaultButtonPadding =
PaddingValues(
start = 12.dp / 2,
top = 10.dp / 2,
end = 16.dp / 2,
bottom = 10.dp / 2,
)
object Cards { object Cards {
val height2x3 = 172.dp val height2x3 = 172.dp
val playedPercentHeight = 6.dp val playedPercentHeight = 6.dp

View file

@ -0,0 +1,173 @@
// This file was inspired by related Button source files from
// https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/tv/tv-material/src/main/java/androidx/tv/material3
package com.github.damontecres.wholphin.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceBorder
import androidx.tv.material3.ClickableSurfaceColors
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceGlow
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ClickableSurfaceShape
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.ProvideTextStyle
import androidx.tv.material3.Surface
import androidx.tv.material3.Text
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
scale: ClickableSurfaceScale = ClickableSurfaceDefaults.scale(),
glow: ClickableSurfaceGlow = ClickableSurfaceDefaults.glow(),
shape: ClickableSurfaceShape =
ClickableSurfaceDefaults.shape(
shape = CircleShape,
),
colors: ClickableSurfaceColors =
ClickableSurfaceDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
focusedContainerColor = MaterialTheme.colorScheme.onSurface,
focusedContentColor = MaterialTheme.colorScheme.inverseOnSurface,
pressedContainerColor = MaterialTheme.colorScheme.onSurface,
pressedContentColor = MaterialTheme.colorScheme.inverseOnSurface,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
),
tonalElevation: Dp = 0.dp,
border: ClickableSurfaceBorder =
ClickableSurfaceDefaults.border(
border = Border.None,
focusedBorder = Border.None,
pressedBorder = Border.None,
disabledBorder = Border.None,
focusedDisabledBorder =
Border(
border =
BorderStroke(
width = 1.5.dp,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
),
shape = CircleShape,
),
),
contentPadding: PaddingValues = DefaultButtonPadding,
contentHeight: Dp = MinButtonSize,
interactionSource: MutableInteractionSource? = null,
content: @Composable RowScope.() -> Unit,
) {
Surface(
modifier = modifier.semantics { role = Role.Button },
onClick = onClick,
onLongClick = onLongClick,
enabled = enabled,
scale = scale,
glow = glow,
shape = shape,
colors = colors,
tonalElevation = tonalElevation,
border = border,
interactionSource = interactionSource,
) {
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
Row(
modifier =
Modifier
.padding(contentPadding)
.height(contentHeight),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content,
)
}
}
}
@Composable
@NonRestartableComposable
fun TextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
contentPadding: PaddingValues =
PaddingValues(
start = 8.dp,
top = 4.dp,
end = 8.dp,
bottom = 4.dp,
),
contentHeight: Dp = 32.dp,
interactionSource: MutableInteractionSource? = null,
content: @Composable RowScope.() -> Unit,
) = Button(
onClick = onClick,
modifier = modifier,
onLongClick = onLongClick,
enabled = enabled,
contentPadding = contentPadding,
contentHeight = contentHeight,
interactionSource = interactionSource,
content = content,
)
@Composable
fun TextButton(
@StringRes stringRes: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
contentPadding: PaddingValues =
PaddingValues(
start = 8.dp,
top = 4.dp,
end = 8.dp,
bottom = 4.dp,
),
contentHeight: Dp = 32.dp,
interactionSource: MutableInteractionSource? = null,
) = Button(
onClick = onClick,
modifier = modifier,
onLongClick = onLongClick,
enabled = enabled,
contentPadding = contentPadding,
contentHeight = contentHeight,
interactionSource = interactionSource,
content = {
Text(text = stringResource(stringRes))
},
)
val DefaultButtonPadding =
PaddingValues(
start = 4.dp,
top = 4.dp,
end = 4.dp,
bottom = 4.dp,
)

View file

@ -45,7 +45,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
@ -443,23 +442,17 @@ fun ConfirmDialogContent(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Button( TextButton(
stringRes = R.string.cancel,
onClick = onCancel, onClick = onCancel,
) {
Text(
text = stringResource(R.string.cancel),
) )
} TextButton(
Button( stringRes = R.string.confirm,
onClick = onConfirm, onClick = onConfirm,
) {
Text(
text = stringResource(R.string.confirm),
) )
} }
} }
} }
}
} }
fun chooseVersionParams( fun chooseVersionParams(

View file

@ -10,7 +10,6 @@ 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.ViewModel import androidx.lifecycle.ViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
@ -56,15 +55,12 @@ fun ErrorMessage(
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
) )
Button( TextButton(
stringRes = R.string.send_app_logs,
onClick = { onClick = {
viewModel.sendLogs() viewModel.sendLogs()
}, },
) {
Text(
text = stringResource(R.string.send_app_logs),
) )
}
message?.let { message?.let {
Text( Text(
text = it, text = it,

View file

@ -4,9 +4,13 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@ -18,17 +22,16 @@ import androidx.compose.runtime.getValue
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.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
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 androidx.tv.material3.Text
@ -46,8 +49,9 @@ import com.github.damontecres.wholphin.data.filter.PlayedFilter
import com.github.damontecres.wholphin.data.filter.VideoTypeFilter import com.github.damontecres.wholphin.data.filter.VideoTypeFilter
import com.github.damontecres.wholphin.data.filter.YearFilter import com.github.damontecres.wholphin.data.filter.YearFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.ui.DefaultButtonPadding
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec
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.UUID import java.util.UUID
@ -353,13 +357,23 @@ fun ExpandableFilterButton(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
iconColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified,
) { ) {
val resources = LocalResources.current
val isFocused = interactionSource.collectIsFocusedAsState().value val isFocused = interactionSource.collectIsFocusedAsState().value
Button( Button(
onClick = onClick, onClick = onClick,
modifier = modifier.heightIn(min = 40.dp), modifier =
contentPadding = DefaultButtonPadding, modifier.requiredSizeIn(
minWidth = MinButtonSize,
minHeight = MinButtonSize,
maxHeight = MinButtonSize,
),
contentPadding = PaddingValues(0.dp), // DefaultButtonPadding,
interactionSource = interactionSource, interactionSource = interactionSource,
) {
Box(
modifier =
Modifier
.fillMaxHeight()
.widthIn(min = if (filterCount > 0 || isFocused) (MinButtonSize - 12.dp) else MinButtonSize),
) { ) {
Text( Text(
text = stringResource(R.string.fa_filter), text = stringResource(R.string.fa_filter),
@ -368,20 +382,39 @@ fun ExpandableFilterButton(
fontSize = 16.sp, fontSize = 16.sp,
fontFamily = FontAwesome, fontFamily = FontAwesome,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier, modifier = Modifier.align(Alignment.Center),
) )
}
AnimatedVisibility(filterCount > 0) { AnimatedVisibility(filterCount > 0) {
Text( Text(
text = filterCount.toString(), text = filterCount.toString(),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(end = 4.dp),
) )
} }
AnimatedVisibility(isFocused) { AnimatedVisibility(isFocused) {
Text( Text(
text = stringResource(R.string.filter), text = stringResource(R.string.filter),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp, end = 4.dp),
)
}
}
}
@PreviewTvSpec
@Composable
private fun ExpandableFilterButtonPreview() {
WholphinTheme {
Column {
ExpandableFilterButton(
filterCount = 2,
onClick = {},
)
ExpandableFilterButton(
filterCount = 0,
onClick = {},
) )
} }
} }

View file

@ -6,10 +6,14 @@ import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -18,6 +22,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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.FocusState import androidx.compose.ui.focus.FocusState
@ -31,16 +36,18 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
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 androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.DefaultButtonPadding
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -77,7 +84,8 @@ fun ExpandablePlayButtons(
playOnClick, playOnClick,
Modifier Modifier
.onFocusChanged(buttonOnFocusChanged) .onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus), .focusRequester(firstFocus)
.animateItem(),
) )
} }
item("restart") { item("restart") {
@ -86,7 +94,9 @@ fun ExpandablePlayButtons(
Duration.ZERO, Duration.ZERO,
Icons.Default.Refresh, Icons.Default.Refresh,
playOnClick, playOnClick,
Modifier.onFocusChanged(buttonOnFocusChanged), Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
mirrorIcon = true, mirrorIcon = true,
) )
} }
@ -99,7 +109,8 @@ fun ExpandablePlayButtons(
playOnClick, playOnClick,
Modifier Modifier
.onFocusChanged(buttonOnFocusChanged) .onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus), .focusRequester(firstFocus)
.animateItem(),
) )
} }
} }
@ -110,7 +121,10 @@ fun ExpandablePlayButtons(
title = if (watched) R.string.mark_unwatched else R.string.mark_watched, title = if (watched) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash, iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
onClick = watchOnClick, onClick = watchOnClick,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged), modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
) )
} }
@ -121,7 +135,10 @@ fun ExpandablePlayButtons(
iconStringRes = R.string.fa_heart, iconStringRes = R.string.fa_heart,
onClick = favoriteOnClick, onClick = favoriteOnClick,
iconColor = if (favorite) Color.Red else Color.Unspecified, iconColor = if (favorite) Color.Red else Color.Unspecified,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged), modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
) )
} }
@ -132,12 +149,16 @@ fun ExpandablePlayButtons(
Duration.ZERO, Duration.ZERO,
Icons.Default.MoreVert, Icons.Default.MoreVert,
{ moreOnClick.invoke() }, { moreOnClick.invoke() },
Modifier.onFocusChanged(buttonOnFocusChanged), Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
) )
} }
} }
} }
val MinButtonSize = 40.dp
/** /**
* An icon button typically used in a row for playing media * An icon button typically used in a row for playing media
* *
@ -156,20 +177,36 @@ fun ExpandablePlayButton(
val isFocused = interactionSource.collectIsFocusedAsState().value val isFocused = interactionSource.collectIsFocusedAsState().value
Button( Button(
onClick = { onClick.invoke(resume) }, onClick = { onClick.invoke(resume) },
modifier = modifier, modifier =
modifier.requiredSizeIn(
minWidth = MinButtonSize,
minHeight = MinButtonSize,
maxHeight = MinButtonSize,
),
contentPadding = DefaultButtonPadding, contentPadding = DefaultButtonPadding,
interactionSource = interactionSource, interactionSource = interactionSource,
) {
Box(
modifier =
Modifier
.padding(start = 2.dp, top = 2.dp)
.height(MinButtonSize),
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }), modifier =
Modifier
.size(28.dp)
.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
) )
}
AnimatedVisibility(isFocused) { AnimatedVisibility(isFocused) {
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
Text( Text(
text = stringResource(title), text = stringResource(title),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(end = 4.dp),
) )
} }
} }
@ -190,9 +227,19 @@ fun ExpandableFaButton(
val isFocused = interactionSource.collectIsFocusedAsState().value val isFocused = interactionSource.collectIsFocusedAsState().value
Button( Button(
onClick = onClick, onClick = onClick,
modifier = modifier.heightIn(min = 40.dp), modifier =
modifier.requiredSizeIn(
minWidth = MinButtonSize,
minHeight = MinButtonSize,
maxHeight = MinButtonSize,
),
contentPadding = DefaultButtonPadding, contentPadding = DefaultButtonPadding,
interactionSource = interactionSource, interactionSource = interactionSource,
) {
Box(
modifier =
Modifier
.size(32.dp),
) { ) {
Text( Text(
text = stringResource(iconStringRes), text = stringResource(iconStringRes),
@ -201,13 +248,14 @@ fun ExpandableFaButton(
fontSize = 16.sp, fontSize = 16.sp,
fontFamily = FontAwesome, fontFamily = FontAwesome,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier, modifier = Modifier.align(Alignment.Center),
) )
}
AnimatedVisibility(isFocused) { AnimatedVisibility(isFocused) {
Text( Text(
text = stringResource(title), text = stringResource(title),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(end = 4.dp),
) )
} }
} }
@ -230,3 +278,44 @@ private fun ExpandablePlayButtonsPreview() {
) )
} }
} }
@PreviewTvSpec
@Composable
private fun ViewOptionsPreview() {
val source = remember { PreviewInteractionSource() }
WholphinTheme {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(16.dp),
) {
ExpandablePlayButton(
title = R.string.play,
resume = Duration.ZERO,
icon = Icons.Default.PlayArrow,
onClick = {},
interactionSource = source,
)
ExpandableFaButton(
title = R.string.play,
iconStringRes = R.string.fa_eye,
onClick = {},
modifier = Modifier,
interactionSource = source,
)
Row {
ExpandableFaButton(
title = R.string.mark_unwatched,
iconStringRes = R.string.fa_eye,
onClick = {},
modifier = Modifier,
)
SortByButton(
sortOptions = listOf(),
current = SortAndDirection(ItemSortBy.DEFAULT, SortOrder.ASCENDING),
onSortChange = {},
)
}
}
}
}

View file

@ -13,13 +13,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.tv.material3.Button
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.data.flip import com.github.damontecres.wholphin.ui.data.flip
import com.github.damontecres.wholphin.ui.data.getStringRes import com.github.damontecres.wholphin.ui.data.getStringRes
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
@ -42,7 +43,7 @@ fun SortByButton(
val context = LocalContext.current val context = LocalContext.current
Box(modifier = modifier) { Box(modifier = modifier) {
Button( TextButton(
onClick = { sortByDropDown = true }, onClick = { sortByDropDown = true },
onLongClick = { onLongClick = {
onSortChange.invoke(current.flip()) onSortChange.invoke(current.flip())

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -45,9 +46,9 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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.tv.material3.Button
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -56,6 +57,8 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.playback.isBackwardButton import com.github.damontecres.wholphin.ui.playback.isBackwardButton
import com.github.damontecres.wholphin.ui.playback.isForwardButton import com.github.damontecres.wholphin.ui.playback.isForwardButton
@ -438,6 +441,7 @@ fun AlphabetButtons(
} }
val focusRequesters = remember { List(letters.length) { FocusRequester() } } val focusRequesters = remember { List(letters.length) { FocusRequester() } }
LazyColumn( LazyColumn(
contentPadding = PaddingValues(4.dp),
state = listState, state = listState,
modifier = modifier =
modifier.focusProperties { modifier.focusProperties {
@ -452,7 +456,7 @@ fun AlphabetButtons(
) { index -> ) { index ->
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
Button( TextButton(
modifier = modifier =
Modifier Modifier
.size(24.dp) .size(24.dp)
@ -472,6 +476,8 @@ fun AlphabetButtons(
Text( Text(
text = letters[index].toString(), text = letters[index].toString(),
color = color, color = color,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
) )
} }
} }

View file

@ -38,7 +38,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -47,6 +46,7 @@ import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.PlaylistInfo import com.github.damontecres.wholphin.data.model.PlaylistInfo
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank

View file

@ -28,15 +28,16 @@ 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.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.tv.material3.Button
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 androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
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.ui.components.Button
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.TextButton
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.seasonEpisode import com.github.damontecres.wholphin.ui.seasonEpisode
@ -145,7 +146,7 @@ fun ProgramDialog(
.fillMaxWidth(), .fillMaxWidth(),
) { ) {
if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) { if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) {
Button( TextButton(
onClick = onWatch, onClick = onWatch,
modifier = Modifier, modifier = Modifier,
) { ) {
@ -179,7 +180,7 @@ fun ProgramDialog(
) { ) {
if (dto.isSeries ?: false) { if (dto.isSeries ?: false) {
item { item {
Button( TextButton(
onClick = { onClick = {
if (isSeriesRecording) { if (isSeriesRecording) {
onCancelRecord.invoke(true) onCancelRecord.invoke(true)
@ -221,7 +222,7 @@ fun ProgramDialog(
if (dto.endDate?.isAfter(LocalDateTime.now()) ?: true) { if (dto.endDate?.isAfter(LocalDateTime.now()) ?: true) {
// Only show program specific recording button if it hasn't finished yet // Only show program specific recording button if it hasn't finished yet
item { item {
Button( TextButton(
onClick = { onClick = {
if (isRecording) { if (isRecording) {
onCancelRecord.invoke(false) onCancelRecord.invoke(false)

View file

@ -45,15 +45,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.tv.material3.Button import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ButtonDefaults
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
@ -62,10 +60,14 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppThemeColors import com.github.damontecres.wholphin.preferences.AppThemeColors
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.components.Button
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekBack
import com.github.damontecres.wholphin.ui.seekForward import com.github.damontecres.wholphin.ui.seekForward
import com.github.damontecres.wholphin.ui.skipStringRes import com.github.damontecres.wholphin.ui.skipStringRes
import com.github.damontecres.wholphin.ui.theme.LocalTheme import com.github.damontecres.wholphin.ui.theme.LocalTheme
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -192,7 +194,8 @@ fun PlaybackControls(
modifier = Modifier.align(Alignment.CenterEnd), modifier = Modifier.align(Alignment.CenterEnd),
) { ) {
currentSegment?.let { segment -> currentSegment?.let { segment ->
Button( TextButton(
stringRes = segment.type.skipStringRes,
onClick = { onClick = {
playerControls.seekTo(segment.endTicks.ticks.inWholeMilliseconds) playerControls.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
}, },
@ -200,12 +203,8 @@ fun PlaybackControls(
Modifier Modifier
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.padding(end = 32.dp), .padding(end = 32.dp),
) {
Text(
text = stringResource(segment.type.skipStringRes),
) )
} }
}
RightPlaybackButtons( RightPlaybackButtons(
captionFocusRequester = captionFocusRequester, captionFocusRequester = captionFocusRequester,
settingsFocusRequester = settingsFocusRequester, settingsFocusRequester = settingsFocusRequester,
@ -286,7 +285,7 @@ fun SeekBar(
} }
} }
private val buttonSpacing = 4.dp private val buttonSpacing = 12.dp
@Composable @Composable
fun LeftPlaybackButtons( fun LeftPlaybackButtons(
@ -435,9 +434,9 @@ fun PlaybackButton(
Button( Button(
enabled = enabled, enabled = enabled,
onClick = onClick, onClick = onClick,
shape = ButtonDefaults.shape(CircleShape), // shape = ButtonDefaults.shape(CircleShape),
colors = colors =
ButtonDefaults.colors( ClickableSurfaceDefaults.colors(
containerColor = AppColors.TransparentBlack25, containerColor = AppColors.TransparentBlack25,
focusedContainerColor = selectedColor, focusedContainerColor = selectedColor,
), ),
@ -445,8 +444,7 @@ fun PlaybackButton(
interactionSource = interactionSource, interactionSource = interactionSource,
modifier = modifier =
modifier modifier
.padding(4.dp) .size(36.dp, 36.dp)
.size(44.dp, 44.dp)
.onFocusChanged { onControllerInteraction.invoke() }, .onFocusChanged { onControllerInteraction.invoke() },
) { ) {
Icon( Icon(
@ -543,3 +541,17 @@ fun BottomDialog(
data class MoreButtonOptions( data class MoreButtonOptions(
val options: Map<String, PlaybackAction>, val options: Map<String, PlaybackAction>,
) )
@PreviewTvSpec
@Composable
private fun ButtonPreview() {
WholphinTheme {
Row {
PlaybackButton(
iconRes = R.drawable.baseline_play_arrow_24,
onClick = {},
onControllerInteraction = {},
)
}
}
}

View file

@ -144,6 +144,15 @@ fun PlaybackOverlay(
} else { } else {
null null
} }
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(bottom = 8.dp)
.onGloballyPositioned {
controllerHeight = with(density) { it.size.height.toDp() }
},
) {
Controller( Controller(
title = item?.title, title = item?.title,
subtitle = item?.subtitleLong, subtitle = item?.subtitleLong,
@ -170,12 +179,37 @@ fun PlaybackOverlay(
}, },
currentSegment = currentSegment, currentSegment = currentSegment,
modifier = modifier =
Modifier Modifier,
// Don't use key events because this control has vertical items so up/down is tough to manage // Don't use key events because this control has vertical items so up/down is tough to manage
.onGloballyPositioned {
controllerHeight = with(density) { it.size.height.toDp() }
},
) )
when (nextState) {
OverlayViewState.CHAPTERS ->
Text(
text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
.padding(start = 16.dp, top = 0.dp)
.onFocusChanged {
if (it.isFocused) state = nextState
}.focusable(),
)
OverlayViewState.QUEUE ->
Text(
text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
.padding(start = 16.dp, top = 0.dp)
.onFocusChanged {
if (it.isFocused) state = nextState
}.focusable(),
)
else -> Spacer(Modifier.height(32.dp))
}
}
} }
AnimatedVisibility( AnimatedVisibility(
state == OverlayViewState.CHAPTERS, state == OverlayViewState.CHAPTERS,
@ -189,8 +223,8 @@ fun PlaybackOverlay(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
Modifier Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp)
.onPreviewKeyEvent { e -> .onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyUp && isUp(e)) { if (e.type == KeyEventType.KeyUp && isUp(e)) {
state = OverlayViewState.CONTROLLER state = OverlayViewState.CONTROLLER
@ -247,7 +281,7 @@ fun PlaybackOverlay(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp, top = 16.dp) .padding(bottom = 8.dp)
.onFocusChanged { .onFocusChanged {
if (it.isFocused) state = OverlayViewState.QUEUE if (it.isFocused) state = OverlayViewState.QUEUE
}.focusable(), }.focusable(),
@ -269,8 +303,8 @@ fun PlaybackOverlay(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
Modifier Modifier
.fillMaxWidth()
.padding(8.dp) .padding(8.dp)
.fillMaxWidth()
.onPreviewKeyEvent { e -> .onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyUp && isUp(e)) { if (e.type == KeyEventType.KeyUp && isUp(e)) {
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
@ -291,7 +325,7 @@ fun PlaybackOverlay(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
LazyRow( LazyRow(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier =
Modifier Modifier
@ -538,32 +572,5 @@ fun Controller(
captionFocusRequester = captionFocusRequester, captionFocusRequester = captionFocusRequester,
settingsFocusRequester = settingsFocusRequester, settingsFocusRequester = settingsFocusRequester,
) )
when (nextState) {
OverlayViewState.CHAPTERS ->
Text(
text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
.padding(start = 16.dp, top = 16.dp)
.onFocusChanged {
if (it.isFocused) onNextStateFocus.invoke()
}.focusable(),
)
OverlayViewState.QUEUE ->
Text(
text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
.padding(start = 16.dp, top = 16.dp)
.onFocusChanged {
if (it.isFocused) onNextStateFocus.invoke()
}.focusable(),
)
else -> Spacer(Modifier.height(32.dp))
}
} }
} }

View file

@ -42,7 +42,6 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@ -57,9 +56,7 @@ import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW
import androidx.media3.ui.compose.modifiers.resizeWithContentScale import androidx.media3.ui.compose.modifiers.resizeWithContentScale
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
import androidx.media3.ui.compose.state.rememberPresentationState import androidx.media3.ui.compose.state.rememberPresentationState
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
@ -71,6 +68,7 @@ import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
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.TextButton
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize
@ -403,19 +401,16 @@ fun PlaybackPage(
delay(10.seconds) delay(10.seconds)
segmentCancelled = true segmentCancelled = true
} }
Button( TextButton(
stringRes = segment.type.skipStringRes,
onClick = { onClick = {
segmentCancelled = true segmentCancelled = true
player.seekTo(segment.endTicks.ticks.inWholeMilliseconds) player.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
}, },
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
) {
Text(
text = stringResource(segment.type.skipStringRes),
) )
} }
} }
}
// Next up episode // Next up episode
BackHandler(nextUp != null) { BackHandler(nextUp != null) {

View file

@ -22,13 +22,13 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.ConfirmDialog import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.components.TextButton
@Composable @Composable
fun StringInputDialog( fun StringInputDialog(
@ -95,26 +95,20 @@ fun StringInputDialog(
horizontalArrangement = Arrangement.SpaceAround, horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Button( TextButton(
stringRes = R.string.cancel,
onClick = onDismissRequest, onClick = onDismissRequest,
modifier = Modifier, modifier = Modifier,
) {
Text(
text = stringResource(R.string.cancel),
) )
} TextButton(
Button( stringRes = R.string.save,
onClick = onDone, onClick = onDone,
modifier = Modifier, modifier = Modifier,
) {
Text(
text = stringResource(R.string.save),
) )
} }
} }
} }
} }
}
if (showConfirm) { if (showConfirm) {
ConfirmDialog( ConfirmDialog(

View file

@ -42,7 +42,6 @@ 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.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
@ -54,8 +53,10 @@ import com.github.damontecres.wholphin.services.Release
import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.Button
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.TextButton
import com.github.damontecres.wholphin.ui.dimAndBlur import com.github.damontecres.wholphin.ui.dimAndBlur
import com.github.damontecres.wholphin.ui.formatBytes import com.github.damontecres.wholphin.ui.formatBytes
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
@ -299,22 +300,16 @@ fun InstallUpdatePageContent(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
Button( TextButton(
stringRes = R.string.download_and_update,
onClick = onInstallRelease, onClick = onInstallRelease,
) {
Text(
text = stringResource(R.string.download_and_update),
) )
} TextButton(
Button( stringRes = R.string.cancel,
onClick = onCancel, onClick = onCancel,
) {
Text(
text = stringResource(R.string.cancel),
) )
} }
} }
}
} }
@Composable @Composable

View file

@ -27,13 +27,14 @@ import androidx.compose.ui.input.key.type
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.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.playback.isEnterKey import com.github.damontecres.wholphin.ui.playback.isEnterKey
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@ -57,7 +58,7 @@ fun PinEntry(
PinArrowRow(Modifier.align(Alignment.CenterHorizontally)) PinArrowRow(Modifier.align(Alignment.CenterHorizontally))
PinEntryDots(input.length, Modifier.align(Alignment.CenterHorizontally)) PinEntryDots(input.length, Modifier.align(Alignment.CenterHorizontally))
Button( TextButton(
onClick = onClickServerAuth, onClick = onClickServerAuth,
modifier = modifier =
Modifier Modifier

View file

@ -29,7 +29,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
@ -37,6 +36,7 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.dimAndBlur import com.github.damontecres.wholphin.ui.dimAndBlur
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
@ -224,7 +224,7 @@ fun SwitchServerContent(
else -> {} else -> {}
} }
Button( TextButton(
onClick = { submit.invoke() }, onClick = { submit.invoke() },
enabled = url.text.isNotNullOrBlank() && state == LoadingState.Pending, enabled = url.text.isNotNullOrBlank() && state == LoadingState.Pending,
modifier = Modifier, modifier = Modifier,

View file

@ -34,7 +34,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
@ -44,6 +43,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.dimAndBlur import com.github.damontecres.wholphin.ui.dimAndBlur
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
@ -203,16 +203,15 @@ fun SwitchUserContent(
) )
} }
UserStateError(userState) UserStateError(userState)
Button( TextButton(
stringRes = R.string.username_or_password,
onClick = { onClick = {
viewModel.cancelQuickConnect() viewModel.cancelQuickConnect()
viewModel.clearSwitchUserState() viewModel.clearSwitchUserState()
useQuickConnect = false useQuickConnect = false
}, },
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
) { )
Text(text = "Use username/password")
}
} else { } else {
val username = rememberTextFieldState() val username = rememberTextFieldState()
val password = rememberTextFieldState() val password = rememberTextFieldState()
@ -282,27 +281,25 @@ fun SwitchUserContent(
modifier = Modifier.focusRequester(passwordFocusRequester), modifier = Modifier.focusRequester(passwordFocusRequester),
) )
} }
Button( TextButton(
stringRes = R.string.login,
onClick = { onSubmit.invoke() }, onClick = { onSubmit.invoke() },
enabled = username.text.isNotNullOrBlank() && password.text.isNotNullOrBlank(), enabled = username.text.isNotNullOrBlank() && password.text.isNotNullOrBlank(),
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
) { )
Text("Login")
}
} }
if (loginAttempts > 2) { if (loginAttempts > 2) {
Text( Text(
text = "Trouble logging in?", text = "Trouble logging in?",
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
) )
Button( TextButton(
stringRes = R.string.show_debug_info,
onClick = { onClick = {
viewModel.navigationManager.navigateTo(Destination.Debug) viewModel.navigationManager.navigateTo(Destination.Debug)
}, },
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
) { )
Text("Show debug info")
}
} }
} }
} }

View file

@ -350,6 +350,8 @@
<string name="cast_and_crew"><![CDATA[Cast & Crew]]></string> <string name="cast_and_crew"><![CDATA[Cast & Crew]]></string>
<string name="guest_stars">Guest stars</string> <string name="guest_stars">Guest stars</string>
<string name="edge_size">Edge size</string> <string name="edge_size">Edge size</string>
<string name="username_or_password">Use username/password</string>
<string name="login">Login</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">