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
import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font
@ -66,14 +65,6 @@ val SlimItemFields =
ItemFields.SORT_NAME,
)
val DefaultButtonPadding =
PaddingValues(
start = 12.dp / 2,
top = 10.dp / 2,
end = 16.dp / 2,
bottom = 10.dp / 2,
)
object Cards {
val height2x3 = 172.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.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
import androidx.tv.material3.LocalContentColor
@ -443,24 +442,18 @@ fun ConfirmDialogContent(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(),
) {
Button(
TextButton(
stringRes = R.string.cancel,
onClick = onCancel,
) {
Text(
text = stringResource(R.string.cancel),
)
}
Button(
TextButton(
stringRes = R.string.confirm,
onClick = onConfirm,
) {
Text(
text = stringResource(R.string.confirm),
)
}
}
}
}
}
fun chooseVersionParams(
context: Context,

View file

@ -10,7 +10,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
@ -56,15 +55,12 @@ fun ErrorMessage(
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium,
)
Button(
TextButton(
stringRes = R.string.send_app_logs,
onClick = {
viewModel.sendLogs()
},
) {
Text(
text = stringResource(R.string.send_app_logs),
)
}
message?.let {
Text(
text = it,

View file

@ -4,9 +4,13 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
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.requiredSizeIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
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.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.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
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.YearFilter
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.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import java.util.UUID
@ -353,13 +357,23 @@ fun ExpandableFilterButton(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
iconColor: Color = Color.Unspecified,
) {
val resources = LocalResources.current
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = onClick,
modifier = modifier.heightIn(min = 40.dp),
contentPadding = DefaultButtonPadding,
modifier =
modifier.requiredSizeIn(
minWidth = MinButtonSize,
minHeight = MinButtonSize,
maxHeight = MinButtonSize,
),
contentPadding = PaddingValues(0.dp), // DefaultButtonPadding,
interactionSource = interactionSource,
) {
Box(
modifier =
Modifier
.fillMaxHeight()
.widthIn(min = if (filterCount > 0 || isFocused) (MinButtonSize - 12.dp) else MinButtonSize),
) {
Text(
text = stringResource(R.string.fa_filter),
@ -368,20 +382,39 @@ fun ExpandableFilterButton(
fontSize = 16.sp,
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier,
modifier = Modifier.align(Alignment.Center),
)
}
AnimatedVisibility(filterCount > 0) {
Text(
text = filterCount.toString(),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 4.dp),
modifier = Modifier.padding(end = 4.dp),
)
}
AnimatedVisibility(isFocused) {
Text(
text = stringResource(R.string.filter),
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.collectIsFocusedAsState
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.Row
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.requiredSizeIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
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.PreviewTvSpec
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
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.Companion.seconds
@ -77,7 +84,8 @@ fun ExpandablePlayButtons(
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus),
.focusRequester(firstFocus)
.animateItem(),
)
}
item("restart") {
@ -86,7 +94,9 @@ fun ExpandablePlayButtons(
Duration.ZERO,
Icons.Default.Refresh,
playOnClick,
Modifier.onFocusChanged(buttonOnFocusChanged),
Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
mirrorIcon = true,
)
}
@ -99,7 +109,8 @@ fun ExpandablePlayButtons(
playOnClick,
Modifier
.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,
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
onClick = watchOnClick,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
)
}
@ -121,7 +135,10 @@ fun ExpandablePlayButtons(
iconStringRes = R.string.fa_heart,
onClick = favoriteOnClick,
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,
Icons.Default.MoreVert,
{ moreOnClick.invoke() },
Modifier.onFocusChanged(buttonOnFocusChanged),
Modifier
.onFocusChanged(buttonOnFocusChanged)
.animateItem(),
)
}
}
}
val MinButtonSize = 40.dp
/**
* An icon button typically used in a row for playing media
*
@ -156,20 +177,36 @@ fun ExpandablePlayButton(
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = { onClick.invoke(resume) },
modifier = modifier,
modifier =
modifier.requiredSizeIn(
minWidth = MinButtonSize,
minHeight = MinButtonSize,
maxHeight = MinButtonSize,
),
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Box(
modifier =
Modifier
.padding(start = 2.dp, top = 2.dp)
.height(MinButtonSize),
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
modifier =
Modifier
.size(28.dp)
.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
)
}
AnimatedVisibility(isFocused) {
Spacer(Modifier.size(8.dp))
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(end = 4.dp),
)
}
}
@ -190,9 +227,19 @@ fun ExpandableFaButton(
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = onClick,
modifier = modifier.heightIn(min = 40.dp),
modifier =
modifier.requiredSizeIn(
minWidth = MinButtonSize,
minHeight = MinButtonSize,
maxHeight = MinButtonSize,
),
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Box(
modifier =
Modifier
.size(32.dp),
) {
Text(
text = stringResource(iconStringRes),
@ -201,13 +248,14 @@ fun ExpandableFaButton(
fontSize = 16.sp,
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier,
modifier = Modifier.align(Alignment.Center),
)
}
AnimatedVisibility(isFocused) {
Text(
text = stringResource(title),
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.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.tv.material3.Button
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
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.flip
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.SortOrder
@ -42,7 +43,7 @@ fun SortByButton(
val context = LocalContext.current
Box(modifier = modifier) {
Button(
TextButton(
onClick = { sortByDropDown = true },
onLongClick = {
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.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.platform.LocalContext
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.tv.material3.Button
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
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.FontAwesome
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.playback.isBackwardButton
import com.github.damontecres.wholphin.ui.playback.isForwardButton
@ -438,6 +441,7 @@ fun AlphabetButtons(
}
val focusRequesters = remember { List(letters.length) { FocusRequester() } }
LazyColumn(
contentPadding = PaddingValues(4.dp),
state = listState,
modifier =
modifier.focusProperties {
@ -452,7 +456,7 @@ fun AlphabetButtons(
) { index ->
val interactionSource = remember { MutableInteractionSource() }
val focused by interactionSource.collectIsFocusedAsState()
Button(
TextButton(
modifier =
Modifier
.size(24.dp)
@ -472,6 +476,8 @@ fun AlphabetButtons(
Text(
text = letters[index].toString(),
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.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
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.data.model.PlaylistInfo
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.ErrorMessage
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.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.seasonEpisode
@ -145,7 +146,7 @@ fun ProgramDialog(
.fillMaxWidth(),
) {
if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) {
Button(
TextButton(
onClick = onWatch,
modifier = Modifier,
) {
@ -179,7 +180,7 @@ fun ProgramDialog(
) {
if (dto.isSeries ?: false) {
item {
Button(
TextButton(
onClick = {
if (isSeriesRecording) {
onCancelRecord.invoke(true)
@ -221,7 +222,7 @@ fun ProgramDialog(
if (dto.endDate?.isAfter(LocalDateTime.now()) ?: true) {
// Only show program specific recording button if it hasn't finished yet
item {
Button(
TextButton(
onClick = {
if (isRecording) {
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.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.tv.material3.Button
import androidx.tv.material3.ButtonDefaults
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
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.preferences.AppThemeColors
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.seekForward
import com.github.damontecres.wholphin.ui.skipStringRes
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.util.ExceptionHandler
import kotlinx.coroutines.delay
@ -192,7 +194,8 @@ fun PlaybackControls(
modifier = Modifier.align(Alignment.CenterEnd),
) {
currentSegment?.let { segment ->
Button(
TextButton(
stringRes = segment.type.skipStringRes,
onClick = {
playerControls.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
},
@ -200,12 +203,8 @@ fun PlaybackControls(
Modifier
.align(Alignment.CenterVertically)
.padding(end = 32.dp),
) {
Text(
text = stringResource(segment.type.skipStringRes),
)
}
}
RightPlaybackButtons(
captionFocusRequester = captionFocusRequester,
settingsFocusRequester = settingsFocusRequester,
@ -286,7 +285,7 @@ fun SeekBar(
}
}
private val buttonSpacing = 4.dp
private val buttonSpacing = 12.dp
@Composable
fun LeftPlaybackButtons(
@ -435,9 +434,9 @@ fun PlaybackButton(
Button(
enabled = enabled,
onClick = onClick,
shape = ButtonDefaults.shape(CircleShape),
// shape = ButtonDefaults.shape(CircleShape),
colors =
ButtonDefaults.colors(
ClickableSurfaceDefaults.colors(
containerColor = AppColors.TransparentBlack25,
focusedContainerColor = selectedColor,
),
@ -445,8 +444,7 @@ fun PlaybackButton(
interactionSource = interactionSource,
modifier =
modifier
.padding(4.dp)
.size(44.dp, 44.dp)
.size(36.dp, 36.dp)
.onFocusChanged { onControllerInteraction.invoke() },
) {
Icon(
@ -543,3 +541,17 @@ fun BottomDialog(
data class MoreButtonOptions(
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 {
null
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(bottom = 8.dp)
.onGloballyPositioned {
controllerHeight = with(density) { it.size.height.toDp() }
},
) {
Controller(
title = item?.title,
subtitle = item?.subtitleLong,
@ -170,12 +179,37 @@ fun PlaybackOverlay(
},
currentSegment = currentSegment,
modifier =
Modifier
Modifier,
// 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(
state == OverlayViewState.CHAPTERS,
@ -189,8 +223,8 @@ fun PlaybackOverlay(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
.padding(8.dp)
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyUp && isUp(e)) {
state = OverlayViewState.CONTROLLER
@ -247,7 +281,7 @@ fun PlaybackOverlay(
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
.padding(start = 16.dp, top = 16.dp)
.padding(bottom = 8.dp)
.onFocusChanged {
if (it.isFocused) state = OverlayViewState.QUEUE
}.focusable(),
@ -269,8 +303,8 @@ fun PlaybackOverlay(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.fillMaxWidth()
.padding(8.dp)
.fillMaxWidth()
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyUp && isUp(e)) {
if (chapters.isNotEmpty()) {
@ -291,7 +325,7 @@ fun PlaybackOverlay(
style = MaterialTheme.typography.titleLarge,
)
LazyRow(
contentPadding = PaddingValues(16.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier =
Modifier
@ -538,32 +572,5 @@ fun Controller(
captionFocusRequester = captionFocusRequester,
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.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
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.state.rememberPlayPauseButtonState
import androidx.media3.ui.compose.state.rememberPresentationState
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.data.model.ItemPlayback
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.components.ErrorMessage
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.preferences.subtitle.SubtitleSettings.applyToMpv
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize
@ -403,19 +401,16 @@ fun PlaybackPage(
delay(10.seconds)
segmentCancelled = true
}
Button(
TextButton(
stringRes = segment.type.skipStringRes,
onClick = {
segmentCancelled = true
player.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
},
modifier = Modifier.focusRequester(focusRequester),
) {
Text(
text = stringResource(segment.type.skipStringRes),
)
}
}
}
// Next up episode
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.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.components.TextButton
@Composable
fun StringInputDialog(
@ -95,26 +95,20 @@ fun StringInputDialog(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxWidth(),
) {
Button(
TextButton(
stringRes = R.string.cancel,
onClick = onDismissRequest,
modifier = Modifier,
) {
Text(
text = stringResource(R.string.cancel),
)
}
Button(
TextButton(
stringRes = R.string.save,
onClick = onDone,
modifier = Modifier,
) {
Text(
text = stringResource(R.string.save),
)
}
}
}
}
}
if (showConfirm) {
ConfirmDialog(

View file

@ -42,7 +42,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
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.ui.PreviewTvSpec
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.LoadingPage
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.dimAndBlur
import com.github.damontecres.wholphin.ui.formatBytes
import com.github.damontecres.wholphin.ui.setValueOnMain
@ -299,23 +300,17 @@ fun InstallUpdatePageContent(
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Button(
TextButton(
stringRes = R.string.download_and_update,
onClick = onInstallRelease,
) {
Text(
text = stringResource(R.string.download_and_update),
)
}
Button(
TextButton(
stringRes = R.string.cancel,
onClick = onCancel,
) {
Text(
text = stringResource(R.string.cancel),
)
}
}
}
}
@Composable
fun DownloadDialog(

View file

@ -27,13 +27,14 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec
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.theme.WholphinTheme
@ -57,7 +58,7 @@ fun PinEntry(
PinArrowRow(Modifier.align(Alignment.CenterHorizontally))
PinEntryDots(input.length, Modifier.align(Alignment.CenterHorizontally))
Button(
TextButton(
onClick = onClickServerAuth,
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.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
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.CircularProgress
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.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -224,7 +224,7 @@ fun SwitchServerContent(
else -> {}
}
Button(
TextButton(
onClick = { submit.invoke() },
enabled = url.text.isNotNullOrBlank() && state == LoadingState.Pending,
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.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
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.CircularProgress
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.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination
@ -203,16 +203,15 @@ fun SwitchUserContent(
)
}
UserStateError(userState)
Button(
TextButton(
stringRes = R.string.username_or_password,
onClick = {
viewModel.cancelQuickConnect()
viewModel.clearSwitchUserState()
useQuickConnect = false
},
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = "Use username/password")
}
)
} else {
val username = rememberTextFieldState()
val password = rememberTextFieldState()
@ -282,27 +281,25 @@ fun SwitchUserContent(
modifier = Modifier.focusRequester(passwordFocusRequester),
)
}
Button(
TextButton(
stringRes = R.string.login,
onClick = { onSubmit.invoke() },
enabled = username.text.isNotNullOrBlank() && password.text.isNotNullOrBlank(),
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text("Login")
}
)
}
if (loginAttempts > 2) {
Text(
text = "Trouble logging in?",
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Button(
TextButton(
stringRes = R.string.show_debug_info,
onClick = {
viewModel.navigationManager.navigateTo(Destination.Debug)
},
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="guest_stars">Guest stars</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">