diff --git a/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt index 59e608e9..f1eb7e90 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt @@ -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 } diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/dolphin/data/JellyfinServer.kt index 170660eb..e5359357 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/data/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/data/JellyfinServer.kt @@ -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) diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt index 91f52a46..7a0334a5 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt @@ -104,3 +104,5 @@ data class BaseItem( } } } + +val BaseItemDto.aspectRatioFloat: Float? get() = width?.let { w -> height?.let { h -> w.toFloat() / h.toFloat() } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/CoilConfig.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/CoilConfig.kt index c379b682..3361d366 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/CoilConfig.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/CoilConfig.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt index 42242e8a..36cf187b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt index 685e4d47..9a6e4b9a 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt @@ -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()), ) } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt index 9d96d14d..192a5d47 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/GridCard.kt @@ -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 ?: "", diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt index 712ea7b6..02128966 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt @@ -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()), ) } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt index 9fa2763c..92a67704 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/SeasonCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/SeasonCard.kt new file mode 100644 index 00000000..650646ba --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/SeasonCard.kt @@ -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), + ) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButton.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButton.kt index 77fc003b..e9642387 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButton.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButton.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt index d479578f..10c46a2d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt index a2ad4d39..17cb2607 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt @@ -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, @@ -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() } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt index 197f9560..38782c88 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt @@ -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()) { diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt index 59335390..227eaf77 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt index 6927d45b..6da98e3c 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt index 4cbfaaa9..84456eeb 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt @@ -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, ) }, ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt index d4d05182..566eaab1 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt @@ -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), ) }, ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt index e758572a..5eb1f57b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt @@ -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") diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt index 4bbeda71..10795af7 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt @@ -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,