mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Numerous UI tweaks esp cards
This commit is contained in:
parent
616ffe8de3
commit
bb517e0857
20 changed files with 324 additions and 58 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -104,3 +104,5 @@ data class BaseItem(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val BaseItemDto.aspectRatioFloat: Float? get() = width?.let { w -> height?.let { h -> w.toFloat() / h.toFloat() } }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ?: "",
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue