Compare commits

...

6 Commits

13 changed files with 78 additions and 56 deletions

View File

@@ -19,7 +19,7 @@ android {
applicationId = "hu.bbara.purefin.tv"
minSdk = 29
targetSdk = 36
versionCode = 2000003
versionCode = 2000004
versionName = "0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -29,12 +29,13 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.core.model.EpisodeUiModel
import hu.bbara.purefin.core.model.MediaUiModel
import hu.bbara.purefin.core.model.MovieUiModel
import hu.bbara.purefin.core.model.SeriesUiModel
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeBadge
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import java.util.UUID
@Composable
@@ -134,10 +135,17 @@ fun PosterCardContent(
.align(Alignment.TopEnd)
.padding(indicatorPadding),
watched = model.watched,
started = (model.progress ?: 0f) > 0f
)
}
else -> Unit
is SeriesUiModel -> {
UnwatchedEpisodeBadge(
unwatchedCount = model.unwatchedEpisodeCount,
size = 26,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(indicatorPadding),
)
}
}
}
Column(

View File

@@ -3,6 +3,7 @@ package hu.bbara.purefin.ui.screen.home.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -14,14 +15,21 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.DrawerValue
import androidx.tv.material3.Icon
import androidx.tv.material3.NavigationDrawer
@@ -117,6 +125,8 @@ fun TvDrawerHeader(
AnimatedVisibility(visible = expanded) {
Text(
text = "Purefin",
color = MaterialTheme.colorScheme.onSurface,
fontSize = 16.sp,
modifier = Modifier.testTag(TvDrawerTitleTag)
)
}
@@ -132,13 +142,18 @@ private fun androidx.tv.material3.NavigationDrawerScope.TvNavigationDrawerRail(
) {
val expanded = drawerValue == DrawerValue.Open
val scheme = MaterialTheme.colorScheme
var hasDrawerFocus by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxHeight()
.background(scheme.surface.copy(alpha = 0.96f))
.padding(horizontal = 12.dp, vertical = 24.dp)
.selectableGroup(),
.selectableGroup()
.focusGroup()
.onFocusChanged { state ->
hasDrawerFocus = state.isFocused
},
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
TvDrawerHeader(
@@ -152,7 +167,8 @@ private fun androidx.tv.material3.NavigationDrawerScope.TvNavigationDrawerRail(
onClick = { onDestinationSelected(destination.destination) },
modifier = Modifier
.testTag("$TvDrawerItemTagPrefix$index")
.semantics { selected = isSelected },
.semantics { selected = isSelected }
.focusProperties { canFocus = hasDrawerFocus || isSelected },
leadingContent = {
Icon(
imageVector = destination.icon,

View File

@@ -304,7 +304,6 @@ private fun TvEpisodeCard(
} else {
WatchStateBadge(
watched = episode.watched,
started = (episode.progress ?: 0.0) > 0.0,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)

View File

@@ -17,7 +17,7 @@ android {
applicationId = "hu.bbara.purefin"
minSdk = 29
targetSdk = 36
versionCode = 1000008
versionCode = 1000009
versionName = "0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -117,7 +117,6 @@ fun PosterCardContent(
.align(Alignment.TopEnd)
.padding(indicatorPadding),
watched = model.watched,
started = (model.progress ?: 0f) > 0f
)
}
else -> Unit

View File

@@ -26,11 +26,9 @@ fun ContinueWatchingSection(
) {
if (items.isEmpty()) return
val filteredItems = items.filter { !it.watched }
val listState = rememberLazyListState()
LaunchedEffect(filteredItems) {
LaunchedEffect(items) {
listState.scrollToItem(index = 0)
}
@@ -46,7 +44,7 @@ fun ContinueWatchingSection(
modifier = Modifier.fillMaxWidth(),
state = listState
) {
itemsIndexed(items = filteredItems, key = { _, item -> item.id }) { index, item ->
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
ContinueWatchingCard(
item = item,
sharedBoundsKey = homeMediaSharedBoundsKey("continue-$index", item.id),

View File

@@ -6,13 +6,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.EpisodeUiModel
import hu.bbara.purefin.core.model.MediaUiModel
import hu.bbara.purefin.core.model.MovieUiModel
import hu.bbara.purefin.core.model.SeriesUiModel
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeBadge
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.card.MediaImageCard
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource
import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick
import hu.bbara.purefin.core.model.EpisodeUiModel
import hu.bbara.purefin.core.model.MediaUiModel
import hu.bbara.purefin.core.model.MovieUiModel
@Composable
internal fun HomeBrowseCard(
@@ -42,10 +44,16 @@ internal fun HomeBrowseCard(
.align(Alignment.TopEnd)
.padding(8.dp),
watched = uiModel.watched,
started = (uiModel.progress ?: 0f) > 0f
)
}
else -> Unit
is SeriesUiModel -> {
UnwatchedEpisodeBadge(
unwatchedCount = uiModel.unwatchedEpisodeCount,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
)
}
}
}
}

View File

@@ -20,13 +20,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.core.feature.browse.library.LibraryViewModel
import hu.bbara.purefin.core.navigation.LibraryDto
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.card.MediaImageCard
import hu.bbara.purefin.core.model.EpisodeUiModel
import hu.bbara.purefin.core.model.MediaUiModel
import hu.bbara.purefin.core.model.MovieUiModel
import hu.bbara.purefin.core.model.SeriesUiModel
import hu.bbara.purefin.core.navigation.LibraryDto
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.card.MediaImageCard
import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar
@Composable
@@ -98,7 +98,6 @@ internal fun LibraryPosterGrid(
.align(Alignment.TopEnd)
.padding(8.dp),
watched = item.watched,
started = (item.progress ?: 0f) > 0f
)
}
else -> Unit

View File

@@ -59,13 +59,13 @@ import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.feature.content.series.SeriesViewModel
import hu.bbara.purefin.core.image.ArtworkKind
import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.model.CastMember
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Season
import hu.bbara.purefin.model.Series
import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.navigation.LocalNavigationManager
import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.player.PlayerActivity
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
@@ -376,7 +376,6 @@ private fun EpisodeCard(
} else {
WatchStateBadge(
watched = episode.watched,
started = (episode.progress ?: 0.0) > 0.0,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)

View File

@@ -21,7 +21,7 @@ fun UnwatchedEpisodeBadge(
unwatchedCount: Int,
foregroundColor: Color = MaterialTheme.colorScheme.onPrimary,
backgroundColor: Color = MaterialTheme.colorScheme.primary,
size: Int = 24,
size: Int = 28,
modifier: Modifier = Modifier
) {
if (unwatchedCount == 0) {
@@ -31,16 +31,16 @@ fun UnwatchedEpisodeBadge(
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.border(1.dp, backgroundColor.copy(alpha = 0.8f), CircleShape)
.background(backgroundColor.copy(alpha = 0.8f), CircleShape)
.border(1.dp, backgroundColor, CircleShape)
.background(backgroundColor, CircleShape)
.size(size.dp)
.clip(CircleShape)
) {
Text(
text = if (unwatchedCount > 9) "9+" else unwatchedCount.toString(),
color = foregroundColor.copy(alpha = 0.8f),
fontWeight = FontWeight.W900,
fontSize = 15.sp
text = if (unwatchedCount > 99) "99+" else unwatchedCount.toString(),
color = foregroundColor,
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
}
}

View File

@@ -22,42 +22,33 @@ import androidx.compose.ui.unit.dp
@Composable
fun WatchStateBadge(
watched: Boolean,
started: Boolean,
watchedColor: Color = MaterialTheme.colorScheme.onPrimary,
watchedBackgroundColor: Color = MaterialTheme.colorScheme.primary,
startedColor: Color = MaterialTheme.colorScheme.onSecondary,
startedBackgroundColor: Color = MaterialTheme.colorScheme.secondary,
size: Int = 24,
modifier: Modifier = Modifier
) {
if (!watched && !started) {
if (!watched) {
return
}
val foregroundColor = if (watched) watchedColor.copy(alpha = 0.8f) else startedColor.copy(alpha = 0.3f)
val backgroundColor = if (watched) watchedBackgroundColor.copy(alpha = 0.8f) else startedBackgroundColor.copy(alpha = 0.3f)
val borderColor = if (watched) watchedBackgroundColor.copy(alpha = 0.8f) else startedBackgroundColor.copy(alpha = 0.8f)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.border(1.dp, borderColor, CircleShape)
.background(backgroundColor, CircleShape)
.border(1.dp, watchedBackgroundColor, CircleShape)
.background(watchedBackgroundColor, CircleShape)
.size(size.dp)
.clip(CircleShape)
) {
if (watched) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "Check",
tint = foregroundColor,
tint = watchedColor,
modifier = Modifier
.padding(1.dp)
.matchParentSize()
)
}
}
}
@Preview
@Composable
@@ -65,15 +56,12 @@ private fun WatchStateBadgePreview() {
Column {
WatchStateBadge(
watched = false,
started = false
)
WatchStateBadge(
watched = true,
started = false
)
WatchStateBadge(
watched = false,
started = true
)
}
}

View File

@@ -26,6 +26,7 @@ class MovieUiModel: MediaUiModel {
override val secondaryText: String
override val description: String
private val prefixImageUrl: String
override val watched: Boolean
override val primaryImageUrl: String
get() {
return ImageUrlBuilder.finishImageUrl(
@@ -49,6 +50,7 @@ class MovieUiModel: MediaUiModel {
description = movie.synopsis
prefixImageUrl = movie.imageUrlPrefix
progress = (movie.progress?.toFloat() ?: 0f) / 100f
watched = movie.watched
}
companion object {
@@ -78,7 +80,9 @@ class SeriesUiModel : MediaUiModel {
override val primaryText: String
override val secondaryText: String
override val description: String
override val watched: Boolean
private val prefixImageUrl: String
val unwatchedEpisodeCount: Int
override val primaryImageUrl: String
get() {
return ImageUrlBuilder.finishImageUrl(
@@ -100,6 +104,8 @@ class SeriesUiModel : MediaUiModel {
secondaryText = "${series.seasonCount} seasons"
description = series.synopsis
prefixImageUrl = series.imageUrlPrefix
watched = series.unwatchedEpisodeCount == 0
unwatchedEpisodeCount = series.unwatchedEpisodeCount
}
}
@@ -109,6 +115,7 @@ class EpisodeUiModel : MediaUiModel {
override val secondaryText: String
override val description: String
private val prefixImageUrl: String
override val watched: Boolean
override val primaryImageUrl: String
get() {
return ImageUrlBuilder.finishImageUrl(
@@ -136,5 +143,6 @@ class EpisodeUiModel : MediaUiModel {
progress = (episode.progress?.toFloat() ?: 0f) / 100f
seriesId = episode.seriesId
seasonId = episode.seasonId
watched = episode.watched
}
}