Numerous UI tweaks esp cards

This commit is contained in:
Damontecres 2025-10-09 19:14:31 -04:00
parent 616ffe8de3
commit bb517e0857
No known key found for this signature in database
20 changed files with 324 additions and 58 deletions

View file

@ -13,7 +13,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -39,6 +38,7 @@ import com.github.damontecres.dolphin.ui.theme.DolphinTheme
import com.github.damontecres.dolphin.util.profile.createDeviceProfile
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@ -59,8 +59,8 @@ class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.i("MainActivity.onCreate")
setContent {
val scope = rememberCoroutineScope()
DolphinTheme(true) {
Surface(
modifier =
@ -80,6 +80,7 @@ class MainActivity : AppCompatActivity() {
appPreferences.currentServerId,
appPreferences.currentUserId,
)
Timber.d("MainActivity session restored")
}
isRestoringSession = false
}

View file

@ -11,6 +11,7 @@ import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Relation
import androidx.room.Transaction
import androidx.room.Update
@Entity(tableName = "servers")
data class JellyfinServer(
@ -52,10 +53,13 @@ data class JellyfinServerUsers(
@Dao
interface JellyfinServerDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun addServer(vararg servers: JellyfinServer)
fun addServer(server: JellyfinServer): Long
@Update(onConflict = OnConflictStrategy.ABORT)
fun updateServer(server: JellyfinServer): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUser(vararg users: JellyfinUser)
fun addUser(user: JellyfinUser)
@Query("DELETE FROM servers WHERE id = :serverId")
fun deleteServer(serverId: String)

View file

@ -104,3 +104,5 @@ data class BaseItem(
}
}
}
val BaseItemDto.aspectRatioFloat: Float? get() = width?.let { w -> height?.let { h -> w.toFloat() / h.toFloat() } }

View file

@ -6,6 +6,7 @@ import coil3.annotation.ExperimentalCoilApi
import coil3.compose.setSingletonImageLoaderFactory
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.network.cachecontrol.CacheControlCacheStrategy
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
@ -22,6 +23,7 @@ fun CoilConfig(
setSingletonImageLoaderFactory { ctx ->
ImageLoader
.Builder(ctx)
.memoryCache(MemoryCache.Builder().maxSizePercent(ctx).build())
.diskCache(
DiskCache
.Builder()

View file

@ -1,10 +1,12 @@
package com.github.damontecres.dolphin.ui
import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.github.damontecres.dolphin.R
import org.jellyfin.sdk.model.api.ItemFields
@ -30,6 +32,19 @@ val DefaultItemFields =
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 defaultHeight2x3 = 180.dp
val playedPercentHeight = 6.dp
}
@Preview(
device = "spec:parent=tv_1080p",
backgroundColor = 0xFF383535,

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.runtime.Composable
@ -31,6 +32,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.Cards
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
@Composable
@ -103,7 +105,10 @@ fun BannerCard(
Box(
modifier =
Modifier
.background(AppColors.TransparentBlack50),
.background(
AppColors.TransparentBlack50,
shape = RoundedCornerShape(25),
),
) {
Text(
text = cornerText,
@ -123,7 +128,7 @@ fun BannerCard(
.background(
MaterialTheme.colorScheme.tertiary,
).clip(RectangleShape)
.height(4.dp)
.height(Cards.playedPercentHeight)
.fillMaxWidth((playPercent / 100).toFloat()),
)
}

View file

@ -89,7 +89,10 @@ fun GridCard(
}
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
) {
Text(
text = item?.name ?: "",

View file

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.runtime.Composable
@ -48,6 +48,7 @@ import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.Cards
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.enableMarquee
import com.github.damontecres.dolphin.ui.ifElse
@ -240,7 +241,7 @@ fun ItemCardImage(
fontFamily = FontAwesome,
)
}
if (watched && watchedPercent?.let { it <= 0 || it >= 100 } == true) {
if (watched && (watchedPercent == null || watchedPercent <= 0.0 || watchedPercent >= 100.0)) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
@ -248,27 +249,26 @@ fun ItemCardImage(
modifier =
Modifier
.align(Alignment.TopEnd)
.size(48.dp)
.padding(8.dp),
.size(36.dp)
.padding(4.dp),
)
}
if (unwatchedCount > 0) {
Box(
modifier =
Modifier
.padding(8.dp)
.align(Alignment.TopEnd),
.padding(4.dp)
.background(
MaterialTheme.colorScheme.border,
shape = RoundedCornerShape(25),
).align(Alignment.TopEnd),
) {
Text(
modifier =
Modifier
.background(
MaterialTheme.colorScheme.border,
shape = CircleShape,
),
color = MaterialTheme.colorScheme.onSurface,
text = unwatchedCount.toString(),
fontSize = 20.sp,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
// fontSize = 16.sp,
modifier = Modifier.padding(4.dp),
)
}
}
@ -281,7 +281,7 @@ fun ItemCardImage(
.background(
MaterialTheme.colorScheme.tertiary,
).clip(RectangleShape)
.height(4.dp)
.height(Cards.playedPercentHeight)
.fillMaxWidth((percent / 100.0).toFloat()),
)
}

View file

@ -36,12 +36,12 @@ fun ItemRow(
modifier: Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit,
) -> Unit = { index, item, modifier, onClick, onLongClick ->
) -> Unit = { index, item, mod, onClick, onLongClick ->
ItemCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
modifier = modifier,
modifier = mod,
)
},
focusPair: FocusPair? = null,

View file

@ -0,0 +1,127 @@
package com.github.damontecres.dolphin.ui.cards
import androidx.compose.animation.core.animateDpAsState
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.aspectRatio
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.enableMarquee
import kotlinx.coroutines.delay
@Composable
fun SeasonCard(
item: BaseItem?,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
imageHeight: Dp = Dp.Unspecified,
imageWidth: Dp = Dp.Unspecified,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val dto = item?.data
val focused by interactionSource.collectIsFocusedAsState()
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
var focusedAfterDelay by remember { mutableStateOf(false) }
val hideOverlayDelay = 500L
if (focused) {
LaunchedEffect(Unit) {
delay(hideOverlayDelay)
if (focused) {
focusedAfterDelay = true
} else {
focusedAfterDelay = false
}
}
} else {
focusedAfterDelay = false
}
val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
val width = imageHeight * aspectRatio
val height = imageWidth * (1f / aspectRatio)
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
modifier = modifier.size(width, height),
) {
Card(
modifier =
Modifier
.size(imageWidth, imageHeight)
.aspectRatio(aspectRatio),
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) {
Box(
modifier =
Modifier
.fillMaxSize(),
) {
ItemCardImage(
imageUrl = item?.imageUrl,
name = item?.name,
showOverlay = false,
favorite = dto?.userData?.isFavorite ?: false,
watched = dto?.userData?.played ?: false,
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
watchedPercent = dto?.userData?.playedPercentage,
useFallbackText = false,
modifier =
Modifier
.fillMaxSize(),
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(),
) {
Text(
text = dto?.name ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
Text(
text = item?.data?.productionYear?.toString() ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
}
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -20,7 +21,9 @@ import androidx.tv.material3.ButtonDefaults
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.ui.DefaultButtonPadding
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.ifElse
import kotlin.time.Duration
@Composable
@ -30,6 +33,7 @@ fun PlayButton(
icon: ImageVector,
onClick: (position: Duration) -> Unit,
modifier: Modifier = Modifier,
mirrorIcon: Boolean = false,
) {
Button(
onClick = { onClick.invoke(resume) },
@ -39,6 +43,7 @@ fun PlayButton(
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
)
Spacer(Modifier.size(8.dp))
Text(
@ -56,17 +61,19 @@ fun ExpandablePlayButton(
onClick: (position: Duration) -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
mirrorIcon: Boolean = false,
) {
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = { onClick.invoke(resume) },
modifier = modifier,
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
)
AnimatedVisibility(isFocused) {
Spacer(Modifier.size(8.dp))
@ -89,14 +96,14 @@ fun ExpandableFaButton(
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = onClick,
modifier = modifier.heightIn(min = 44.dp),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
modifier = modifier.heightIn(min = 40.dp),
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Text(
text = stringResource(iconStringRes),
style = MaterialTheme.typography.titleSmall,
fontSize = 18.sp,
fontSize = 16.sp,
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier,

View file

@ -27,8 +27,11 @@ import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ui.PreviewTvSpec
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
import com.github.damontecres.dolphin.ui.tryRequestFocus
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Composable
fun PlayButtons(
@ -70,6 +73,7 @@ fun PlayButtons(
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged),
mirrorIcon = true,
)
}
} else {
@ -154,6 +158,7 @@ fun ExpandablePlayButtons(
Icons.Default.Refresh,
playOnClick,
Modifier.onFocusChanged(buttonOnFocusChanged),
mirrorIcon = true,
)
}
} else {
@ -192,3 +197,19 @@ fun ExpandablePlayButtons(
}
}
}
@PreviewTvSpec
@Composable
private fun ExpandablePlayButtonsPreview() {
DolphinTheme(true) {
ExpandablePlayButtons(
resumePosition = 10.seconds,
watched = false,
playOnClick = {},
watchOnClick = {},
moreOnClick = {},
buttonOnFocusChanged = {},
modifier = Modifier,
)
}
}

View file

@ -1,6 +1,7 @@
package com.github.damontecres.dolphin.ui.detail
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -18,6 +19,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -62,6 +64,7 @@ import timber.log.Timber
private const val DEBUG = false
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CardGrid(
pager: List<BaseItem?>,
@ -78,7 +81,8 @@ fun CardGrid(
val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
val columns = 6
val gridState = rememberLazyGridState()
val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f)
val gridState = rememberLazyGridState(cacheWindow = fractionCacheWindow)
val scope = rememberCoroutineScope()
val firstFocus = remember { FocusRequester() }
val zeroFocus = remember { FocusRequester() }

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
@ -48,8 +49,10 @@ import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.Person
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.Cards
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.cards.PersonRow
import com.github.damontecres.dolphin.ui.cards.SeasonCard
import com.github.damontecres.dolphin.ui.components.ConfirmDialog
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
import com.github.damontecres.dolphin.ui.components.ErrorMessage
@ -232,6 +235,16 @@ fun SeriesDetailsContent(
modifier =
Modifier
.fillMaxWidth(),
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
SeasonCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
modifier = mod,
imageHeight = Cards.defaultHeight2x3,
imageWidth = Dp.Unspecified,
)
},
)
}
if (people.isNotEmpty()) {

View file

@ -118,11 +118,11 @@ class SeriesViewModel
ThemeSongVolume.DISABLED,
-> return
ThemeSongVolume.LOWEST -> .1f
ThemeSongVolume.LOW -> .25f
ThemeSongVolume.MEDIUM -> .5f
ThemeSongVolume.HIGH -> .75f
ThemeSongVolume.HIGHEST -> 1f
ThemeSongVolume.LOWEST -> .05f
ThemeSongVolume.LOW -> .1f
ThemeSongVolume.MEDIUM -> .25f
ThemeSongVolume.HIGH -> .5f
ThemeSongVolume.HIGHEST -> 75f
}
viewModelScope.launch(ExceptionHandler()) {
val themeSongs = api.libraryApi.getThemeSongs(seriesId).content

View file

@ -43,6 +43,7 @@ import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.aspectRatioFloat
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.ui.cards.BannerCard
import com.github.damontecres.dolphin.ui.ifElse
@ -225,12 +226,17 @@ fun SeriesOverviewContent(
),
)
}
BannerCard(
name = episode?.name,
imageUrl = episode?.imageUrl,
aspectRatio =
episode?.data?.primaryImageAspectRatio?.toFloat()
?: (16f / 9),
episode
?.data
?.primaryImageAspectRatio
?.toFloat()
?.coerceAtLeast(episode.data.aspectRatioFloat ?: (4f / 3f))
?: (16f / 9), // TODO some episode images don't match the file's aspect ratio
cornerText = "E${episode?.data?.indexNumber}",
played = episode?.data?.userData?.played ?: false,
playPercent = episode?.data?.userData?.playedPercentage ?: 0.0,

View file

@ -39,6 +39,7 @@ import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.Cards
import com.github.damontecres.dolphin.ui.cards.BannerCard
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
@ -194,7 +195,7 @@ fun HomePageContent(
Modifier.focusRequester(positionFocusRequester),
),
interactionSource = null,
cardHeight = 200.dp,
cardHeight = Cards.defaultHeight2x3,
)
},
)

View file

@ -4,8 +4,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -25,9 +26,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.Cards
import com.github.damontecres.dolphin.ui.DefaultItemFields
import com.github.damontecres.dolphin.ui.cards.EpisodeCard
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.cards.SeasonCard
import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.NavigationManager
@ -125,7 +128,7 @@ fun SearchPage(
val series by viewModel.series.observeAsState(listOf())
val episodes by viewModel.episodes.observeAsState(listOf())
var query by remember { mutableStateOf("") }
var query by rememberSaveable { mutableStateOf("") }
val searchFocusRequester = remember { FocusRequester() }
val resultsFocusRequester = remember { FocusRequester() }
@ -163,22 +166,47 @@ fun SearchPage(
)
}
}
itemsIndexed(listOf(movies, series)) { index, items ->
if (items.isNotEmpty()) {
if (movies.isNotEmpty()) {
item {
ItemRow(
title =
when (index) {
0 -> "Movies"
1 -> "Series"
2 -> "Episodes"
else -> ""
},
items = items,
title = "Movies",
items = movies,
onClickItem = {
viewModel.navigationManager.navigateTo(it.destination())
},
onLongClickItem = {},
modifier = Modifier.focusRequester(resultsFocusRequester),
modifier = Modifier,
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
SeasonCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = Cards.defaultHeight2x3,
modifier = mod,
)
},
)
}
}
if (series.isNotEmpty()) {
item {
ItemRow(
title = "Series",
items = series,
onClickItem = {
viewModel.navigationManager.navigateTo(it.destination())
},
onLongClickItem = {},
modifier = Modifier,
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
SeasonCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = Cards.defaultHeight2x3,
modifier = mod,
)
},
)
}
}
@ -197,8 +225,8 @@ fun SearchPage(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = 160.dp,
modifier = mod,
imageHeight = 140.dp,
modifier = mod.padding(horizontal = 8.dp),
)
},
)

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@ -17,9 +18,12 @@ 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.text.input.KeyboardCapitalization
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
@ -27,7 +31,9 @@ import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.ui.components.BasicDialog
import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable
fun SwitchServerContent(
@ -95,11 +101,17 @@ fun SwitchServerContent(
}
BasicDialog(
onDismissRequest = { showAddServer = false },
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
modifier =
Modifier
.padding(16.dp)
.fillMaxWidth(.4f),
) {
Text(
text = "Enter Server URL",
@ -117,10 +129,14 @@ fun SwitchServerContent(
KeyboardActions(
onDone = { submit.invoke() },
),
modifier = Modifier,
modifier =
Modifier
.focusRequester(focusRequester)
.fillMaxWidth(),
)
Button(
onClick = { submit.invoke() },
enabled = url.isNotNullOrBlank(),
modifier = Modifier,
) {
Text(text = "Submit")

View file

@ -1,6 +1,7 @@
package com.github.damontecres.dolphin.ui.setup
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -19,6 +20,8 @@ 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.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
@ -31,6 +34,7 @@ import com.github.damontecres.dolphin.ui.components.BasicDialog
import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable
fun SwitchUserContent(
@ -152,13 +156,20 @@ fun SwitchUserContent(
viewModel.navigationManager.goToHome()
}
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Text(
text = "Enter username/password to login to ${server.name ?: server.url}",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier,
modifier = Modifier.focusGroup(),
) {
Text(
text = "Username",
modifier = Modifier,
modifier = Modifier.padding(end = 8.dp),
)
EditTextBox(
value = username,
@ -169,7 +180,7 @@ fun SwitchUserContent(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Text,
),
modifier = Modifier,
modifier = Modifier.focusRequester(focusRequester),
)
}
Row(
@ -178,7 +189,7 @@ fun SwitchUserContent(
) {
Text(
text = "Password",
modifier = Modifier,
modifier = Modifier.padding(end = 8.dp),
)
EditTextBox(
value = password,