Compare commits

...

12 Commits

Author SHA1 Message Date
584e430431 fix: use finished image url for PosterItems 2026-04-01 22:56:01 +02:00
680df51ce7 refactor: update image handling in Episode dto to use imageUrlPrefix and JellyfinImageHelper 2026-04-01 22:49:37 +02:00
98850087ff refactor: update image handling in Series dto to use imageUrlPrefix and JellyfinImageHelper 2026-04-01 22:38:58 +02:00
ce5266f17c refactor: update image handling in Movie dto to use imageUrlPrefix and JellyfinImageHelper 2026-04-01 22:30:32 +02:00
d6d05be93f Add image URL prefix and completion helpers to JellyfinImageHelper 2026-04-01 22:17:09 +02:00
61ce6abb39 refactor: reorganize package structure and rename TvHomeScreen to TvHomeContent for clarity 2026-04-01 21:55:46 +02:00
6e4ea692d5 refactor: rename TvHomeContent to TvHomeScreen for consistency 2026-04-01 21:50:49 +02:00
de011a5f3b refactor: rename TvHomeScreen to TvAppScreen for consistency 2026-04-01 21:50:30 +02:00
b3d3f60d78 refactor: simplify navigation 2026-04-01 21:48:58 +02:00
cdb9dc3403 feat: add androidx.tv.material3 dependency for TV-specific UI components 2026-04-01 20:34:20 +02:00
866830b8f4 refactor: simplify image loading in PosterCard and TvHomeSections by removing unnecessary ImageRequest 2026-04-01 17:05:34 +02:00
b90632063c feat: add Settings tab to TV home screen and update color scheme 2026-04-01 16:51:24 +02:00
41 changed files with 396 additions and 1144 deletions

View File

@@ -65,6 +65,7 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.tv.material3)
implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.jellyfin.core) implementation(libs.jellyfin.core)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)

View File

@@ -101,7 +101,7 @@ class EpisodeScreenContentTest {
progress = progress, progress = progress,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = listOf( cast = listOf(
CastMember("Adam Scott", "Mark Scout", null) CastMember("Adam Scott", "Mark Scout", null)
) )

View File

@@ -65,7 +65,7 @@ class MovieScreenContentTest {
runtime = "164m", runtime = "164m",
format = "4K", format = "4K",
synopsis = "Officer K uncovers a secret that sends him searching for Rick Deckard.", synopsis = "Officer K uncovers a secret that sends him searching for Rick Deckard.",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
audioTrack = "ENG 5.1", audioTrack = "ENG 5.1",
subtitles = "ENG", subtitles = "ENG",
cast = listOf( cast = listOf(

View File

@@ -92,7 +92,7 @@ class SeriesScreenContentTest {
name = "Severance", name = "Severance",
synopsis = "Mark leads a team of office workers whose memories have been surgically divided.", synopsis = "Mark leads a team of office workers whose memories have been surgically divided.",
year = "2022", year = "2022",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
unwatchedEpisodeCount = 3, unwatchedEpisodeCount = 3,
seasonCount = 1, seasonCount = 1,
seasons = listOf( seasons = listOf(
@@ -117,7 +117,7 @@ class SeriesScreenContentTest {
progress = 18.0, progress = 18.0,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = emptyList() cast = emptyList()
), ),
Episode( Episode(
@@ -133,7 +133,7 @@ class SeriesScreenContentTest {
progress = null, progress = null,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = emptyList() cast = emptyList()
) )
) )
@@ -154,7 +154,7 @@ class SeriesScreenContentTest {
name = "Foundation", name = "Foundation",
synopsis = "A band of exiles works to preserve knowledge through the fall of an empire.", synopsis = "A band of exiles works to preserve knowledge through the fall of an empire.",
year = "2021", year = "2021",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
unwatchedEpisodeCount = 0, unwatchedEpisodeCount = 0,
seasonCount = 1, seasonCount = 1,
seasons = listOf( seasons = listOf(

View File

@@ -19,12 +19,14 @@ import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
fun EpisodeScreen( fun EpisodeScreen(
@@ -91,7 +93,7 @@ internal fun EpisodeScreenContent(
} }
TvMediaDetailScaffold( TvMediaDetailScaffold(
heroImageUrl = episode.heroImageUrl, heroImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
resetScrollKey = episode.id, resetScrollKey = episode.id,
modifier = modifier, modifier = modifier,
topBar = { topBar = {

View File

@@ -19,9 +19,11 @@ import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.MovieDto import hu.bbara.purefin.core.data.navigation.MovieDto
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
fun MovieScreen( fun MovieScreen(
@@ -60,7 +62,7 @@ internal fun MovieScreenContent(
} }
TvMediaDetailScaffold( TvMediaDetailScaffold(
heroImageUrl = movie.heroImageUrl, heroImageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
resetScrollKey = movie.id, resetScrollKey = movie.id,
modifier = modifier, modifier = modifier,
topBar = { topBar = {

View File

@@ -60,11 +60,13 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.common.ui.components.MediaResumeButton
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
import org.jellyfin.sdk.model.api.ImageType
internal const val SeriesPlayButtonTag = "series-play-button" internal const val SeriesPlayButtonTag = "series-play-button"
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab" internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
@@ -310,7 +312,7 @@ private fun EpisodeCard(
) )
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = episode.heroImageUrl, model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -18,11 +18,13 @@ import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.SeriesDto import hu.bbara.purefin.core.data.navigation.SeriesDto
import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
fun SeriesScreen( fun SeriesScreen(
@@ -78,7 +80,7 @@ internal fun SeriesScreenContent(
} }
TvMediaDetailScaffold( TvMediaDetailScaffold(
heroImageUrl = series.heroImageUrl, heroImageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
resetScrollKey = series.id, resetScrollKey = series.id,
modifier = modifier, modifier = modifier,
topBar = { topBar = {

View File

@@ -20,20 +20,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow 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 androidx.compose.ui.unit.sp
import coil3.request.ImageRequest
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
@@ -45,21 +39,15 @@ import org.jellyfin.sdk.model.api.BaseItemKind
fun PosterCard( fun PosterCard(
item: PosterItem, item: PosterItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val density = LocalDensity.current
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale") val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
val posterWidth = 144.dp val posterWidth = 144.dp
val posterHeight = posterWidth * 3 / 2
fun openItem(posterItem: PosterItem) { fun openItem(posterItem: PosterItem) {
when (posterItem.type) { when (posterItem.type) {
@@ -73,24 +61,6 @@ fun PosterCard(
} }
} }
val imageRequest = ImageRequest.Builder(context)
.data(item.imageUrl)
.size(with(density) { posterWidth.roundToPx() }, with(density) { posterHeight.roundToPx() })
.build()
val imageFocusModifier = Modifier
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.then(
if (upFocusRequester != null || downFocusRequester != null) {
Modifier.focusProperties {
upFocusRequester?.let { up = it }
downFocusRequester?.let { down = it }
}
} else {
Modifier
}
)
Column( Column(
modifier = modifier modifier = modifier
.width(posterWidth) .width(posterWidth)
@@ -102,9 +72,9 @@ fun PosterCard(
) { ) {
Box() { Box() {
PurefinAsyncImage( PurefinAsyncImage(
model = imageRequest, model = item.imageUrl,
contentDescription = null, contentDescription = null,
modifier = imageFocusModifier modifier = Modifier
.aspectRatio(2f / 3f) .aspectRatio(2f / 3f)
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.border( .border(

View File

@@ -0,0 +1,136 @@
package hu.bbara.purefin.tv
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Tv
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect
import hu.bbara.purefin.feature.shared.home.AppViewModel
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
import hu.bbara.purefin.tv.home.TvHomeScreen
import hu.bbara.purefin.tv.home.ui.TvHomeTabDestination
import hu.bbara.purefin.tv.home.ui.TvHomeTabItem
import hu.bbara.purefin.tv.home.ui.TvHomeTopBar
import hu.bbara.purefin.tv.library.ui.TvLibraryContent
import org.jellyfin.sdk.model.api.CollectionType
@Composable
fun TvAppScreen(
viewModel: AppViewModel = hiltViewModel(),
libraryViewModel: LibraryViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
var selectedTabIndex by remember { mutableIntStateOf(1) }
val libraries by viewModel.libraries.collectAsState()
val continueWatching by viewModel.continueWatching.collectAsState()
val nextUp by viewModel.nextUp.collectAsState()
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
val selectedLibraryItems by libraryViewModel.contents.collectAsState()
val tabs = remember(libraries) {
buildList {
add(
TvHomeTabItem(
destination = TvHomeTabDestination.SETTINGS,
label = "Settings",
icon = Icons.Outlined.Settings,
)
)
add(
TvHomeTabItem(
destination = TvHomeTabDestination.SEARCH,
label = "Search",
icon = Icons.Outlined.Search,
)
)
add(
TvHomeTabItem(
destination = TvHomeTabDestination.HOME,
label = "Home",
icon = Icons.Outlined.Home,
)
)
addAll(libraries.map {
TvHomeTabItem(
destination = TvHomeTabDestination.LIBRARY,
label = it.name,
icon = when (it.type) {
CollectionType.MOVIES -> Icons.Outlined.Movie
CollectionType.TVSHOWS -> Icons.Outlined.Tv
else -> Icons.Outlined.Collections
},
libraryId = it.id
)
})
}
}
LifecycleResumeEffect(Unit) {
viewModel.onResumed()
onPauseOrDispose { }
}
val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, (tabs.size - 1).coerceAtLeast(0))
val selectedTab = tabs.getOrNull(safeSelectedTabIndex)
LaunchedEffect(selectedTab?.destination, selectedTab?.libraryId) {
if (selectedTab?.destination == TvHomeTabDestination.LIBRARY) {
val libraryId = selectedTab.libraryId ?: return@LaunchedEffect
libraryViewModel.selectLibrary(libraryId)
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TvHomeTopBar(
tabs = tabs,
selectedTabIndex = safeSelectedTabIndex,
onTabSelected = { index, _ ->
selectedTabIndex = index
}
)
}
) { innerPadding ->
when (selectedTab?.destination) {
TvHomeTabDestination.SETTINGS,
TvHomeTabDestination.LIBRARY -> {
TvLibraryContent(
libraryItems = selectedLibraryItems,
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
modifier = Modifier.padding(innerPadding)
)
}
TvHomeTabDestination.SEARCH,
TvHomeTabDestination.HOME,
null -> {
TvHomeScreen(
libraries = libraries,
libraryContent = latestLibraryContent,
continueWatching = continueWatching,
nextUp = nextUp,
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = viewModel::onEpisodeSelected,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}

View File

@@ -1,128 +1,35 @@
package hu.bbara.purefin.tv.home package hu.bbara.purefin.tv.home
import androidx.compose.foundation.layout.fillMaxSize import android.annotation.SuppressLint
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Tv
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import androidx.lifecycle.compose.LifecycleResumeEffect import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.AppViewModel import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.library.LibraryViewModel import hu.bbara.purefin.feature.shared.home.PosterItem
import hu.bbara.purefin.tv.home.ui.TvHomeContent import hu.bbara.purefin.tv.home.ui.TvHomeContent
import hu.bbara.purefin.tv.home.ui.TvHomeTabDestination import org.jellyfin.sdk.model.UUID
import hu.bbara.purefin.tv.home.ui.TvHomeTabItem
import hu.bbara.purefin.tv.home.ui.TvHomeTopBar
import hu.bbara.purefin.tv.library.ui.TvLibraryContent
import org.jellyfin.sdk.model.api.CollectionType
@SuppressLint("RememberInComposition")
@Composable @Composable
fun TvHomeScreen( fun TvHomeScreen(
viewModel: AppViewModel = hiltViewModel(), libraries: List<LibraryItem>,
libraryViewModel: LibraryViewModel = hiltViewModel(), libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var selectedTabIndex by remember { mutableIntStateOf(1) } TvHomeContent(
val libraries by viewModel.libraries.collectAsState() libraries = libraries,
val continueWatching by viewModel.continueWatching.collectAsState() libraryContent = libraryContent,
val nextUp by viewModel.nextUp.collectAsState() continueWatching = continueWatching,
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState() nextUp = nextUp,
val selectedLibraryItems by libraryViewModel.contents.collectAsState() onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
val tabs = remember(libraries) { onEpisodeSelected = onEpisodeSelected,
buildList { modifier = modifier
add( )
TvHomeTabItem(
destination = TvHomeTabDestination.SEARCH,
label = "Search",
icon = Icons.Outlined.Search,
)
)
add(
TvHomeTabItem(
destination = TvHomeTabDestination.HOME,
label = "Home",
icon = Icons.Outlined.Home,
)
)
addAll(libraries.map {
TvHomeTabItem(
destination = TvHomeTabDestination.LIBRARY,
label = it.name,
icon = when (it.type) {
CollectionType.MOVIES -> Icons.Outlined.Movie
CollectionType.TVSHOWS -> Icons.Outlined.Tv
else -> Icons.Outlined.Collections
},
libraryId = it.id
)
})
}
}
LifecycleResumeEffect(Unit) {
viewModel.onResumed()
onPauseOrDispose { }
}
val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, (tabs.size - 1).coerceAtLeast(0))
val selectedTab = tabs.getOrNull(safeSelectedTabIndex)
LaunchedEffect(selectedTab?.destination, selectedTab?.libraryId) {
if (selectedTab?.destination == TvHomeTabDestination.LIBRARY) {
val libraryId = selectedTab.libraryId ?: return@LaunchedEffect
libraryViewModel.selectLibrary(libraryId)
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TvHomeTopBar(
tabs = tabs,
selectedTabIndex = safeSelectedTabIndex,
onTabSelected = { index, _ ->
selectedTabIndex = index
}
)
}
) { innerPadding ->
when (selectedTab?.destination) {
TvHomeTabDestination.LIBRARY -> {
TvLibraryContent(
libraryItems = selectedLibraryItems,
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
modifier = Modifier.padding(innerPadding)
)
}
TvHomeTabDestination.SEARCH,
TvHomeTabDestination.HOME,
null -> {
TvHomeContent(
libraries = libraries,
libraryContent = latestLibraryContent,
continueWatching = continueWatching,
nextUp = nextUp,
onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = viewModel::onEpisodeSelected,
modifier = Modifier.padding(innerPadding)
)
}
}
}
} }

View File

@@ -8,10 +8,13 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.LibraryItem
@@ -33,35 +36,10 @@ fun TvHomeContent(
val visibleLibraries = remember(libraries, libraryContent) { val visibleLibraries = remember(libraries, libraryContent) {
libraries.filter { libraryContent[it.id]?.isEmpty() != true } libraries.filter { libraryContent[it.id]?.isEmpty() != true }
} }
val continueWatchingSectionFocusRequester = remember { FocusRequester() }
val continueWatchingFirstItemFocusRequester = remember { FocusRequester() }
val nextUpSectionFocusRequester = remember { FocusRequester() }
val nextUpFirstItemFocusRequester = remember { FocusRequester() }
val librarySectionFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { library -> library.id to FocusRequester() }
}
val libraryFirstItemFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { library -> library.id to FocusRequester() }
}
val firstSectionFocusRequester = when {
continueWatching.isNotEmpty() -> continueWatchingSectionFocusRequester
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
else -> visibleLibraries.firstOrNull()?.let { librarySectionFocusRequesters[it.id] }
}
val firstVisibleLibrarySectionFocusRequester = visibleLibraries.firstOrNull()
?.let { librarySectionFocusRequesters[it.id] }
val firstHomeRowBelowContinueWatching = when {
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
else -> firstVisibleLibrarySectionFocusRequester
}
val firstHomeRowAboveLibraries = when {
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
continueWatching.isNotEmpty() -> continueWatchingSectionFocusRequester
else -> null
}
LaunchedEffect(firstSectionFocusRequester) { val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() }
firstSectionFocusRequester?.requestFocus() val libraryRefs = remember(libraryContent) {
libraryContent.keys.associateWith { FocusRequester() }
} }
LazyColumn( LazyColumn(
@@ -75,11 +53,12 @@ fun TvHomeContent(
item { item {
TvContinueWatchingSection( TvContinueWatchingSection(
items = continueWatching, items = continueWatching,
sectionFocusRequester = continueWatchingSectionFocusRequester,
firstItemFocusRequester = continueWatchingFirstItemFocusRequester,
downFocusRequester = firstHomeRowBelowContinueWatching,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected,
modifier = Modifier.focusRequester(continueWatchingRef)
.focusProperties {
down = nextUpRef
}
) )
} }
item { item {
@@ -88,12 +67,12 @@ fun TvHomeContent(
item { item {
TvNextUpSection( TvNextUpSection(
items = nextUp, items = nextUp,
sectionFocusRequester = nextUpSectionFocusRequester, onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = nextUpFirstItemFocusRequester, modifier = Modifier.focusRequester(nextUpRef)
upFocusRequester = continueWatching.takeIf { it.isNotEmpty() } .focusProperties {
?.let { continueWatchingSectionFocusRequester }, up = continueWatchingRef
downFocusRequester = firstVisibleLibrarySectionFocusRequester, libraryRefs.values.firstOrNull()?.let { down = it }
onEpisodeSelected = onEpisodeSelected }
) )
} }
item { item {
@@ -103,31 +82,22 @@ fun TvHomeContent(
items = visibleLibraries, items = visibleLibraries,
key = { it.id } key = { it.id }
) { item -> ) { item ->
val libraryIndex = visibleLibraries.indexOfFirst { it.id == item.id } val ref = libraryRefs[item.id]
val previousLibrarySectionFocusRequester = visibleLibraries
.getOrNull(libraryIndex - 1)
?.let { librarySectionFocusRequesters[it.id] }
val nextLibrarySectionFocusRequester = visibleLibraries
.getOrNull(libraryIndex + 1)
?.let { librarySectionFocusRequesters[it.id] }
TvLibraryPosterSection( TvLibraryPosterSection(
title = item.name, title = item.name,
items = libraryContent[item.id] ?: emptyList(), items = libraryContent[item.id] ?: emptyList(),
action = "See All", action = "See All",
sectionFocusRequester = librarySectionFocusRequesters.getValue(item.id),
firstItemFocusRequester = libraryFirstItemFocusRequesters.getValue(item.id),
upFocusRequester = if (libraryIndex == 0) {
firstHomeRowAboveLibraries
} else {
previousLibrarySectionFocusRequester
},
downFocusRequester = nextLibrarySectionFocusRequester,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected, onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected,
modifier = if (ref != null) Modifier.focusRequester(ref) else Modifier
.focusProperties {
up = nextUpRef
libraryRefs.values.dropWhile { it != ref }.drop(1).firstOrNull()
?.let { down = it }
}
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
} }

View File

@@ -13,7 +13,8 @@ data class TvHomeNavItem(
enum class TvHomeTabDestination { enum class TvHomeTabDestination {
SEARCH, SEARCH,
HOME, HOME,
LIBRARY LIBRARY,
SETTINGS
} }
data class TvHomeTabItem( data class TvHomeTabItem(

View File

@@ -28,38 +28,29 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow 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 androidx.compose.ui.unit.sp
import coil3.request.ImageRequest
import hu.bbara.purefin.common.ui.PosterCard import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.MediaProgressBar import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import kotlin.math.nextUp import kotlin.math.nextUp
@Composable @Composable
fun TvContinueWatchingSection( fun TvContinueWatchingSection(
items: List<ContinueWatchingItem>, items: List<ContinueWatchingItem>,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -71,20 +62,13 @@ fun TvContinueWatchingSection(
) )
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
itemsIndexed(items = items) { index, item -> itemsIndexed(items = items) { index, item ->
TvContinueWatchingCard( TvContinueWatchingCard(
item = item, item = item,
focusRequester = if (index == 0) firstItemFocusRequester else null,
isFirstItem = index == 0,
isLastItem = index == items.lastIndex,
upFocusRequester = upFocusRequester,
downFocusRequester = downFocusRequester,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected
) )
@@ -96,29 +80,27 @@ fun TvContinueWatchingSection(
fun TvContinueWatchingCard( fun TvContinueWatchingCard(
item: ContinueWatchingItem, item: ContinueWatchingItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
isFirstItem: Boolean = false,
isLastItem: Boolean = false,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val density = LocalDensity.current
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale") val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
val imageUrl = when (item.type) { val imageUrl = when (item.type) {
BaseItemKind.MOVIE -> item.movie?.heroImageUrl BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
BaseItemKind.EPISODE -> item.episode?.heroImageUrl prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
else -> null else -> null
} }
val cardWidth = 280.dp val cardWidth = 280.dp
val cardHeight = cardWidth * 9 / 16
fun openItem(item: ContinueWatchingItem) { fun openItem(item: ContinueWatchingItem) {
when (item.type) { when (item.type) {
@@ -132,37 +114,6 @@ fun TvContinueWatchingCard(
} }
} }
val imageRequest = ImageRequest.Builder(context)
.data(imageUrl)
.size(with(density) { cardWidth.roundToPx() }, with(density) { cardHeight.roundToPx() })
.build()
val imageFocusModifier = Modifier
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.then(
if (upFocusRequester != null || downFocusRequester != null) {
Modifier.focusProperties {
upFocusRequester?.let { up = it }
downFocusRequester?.let { down = it }
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
} else {
Modifier.focusProperties {
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
}
)
Column( Column(
modifier = modifier modifier = modifier
.width(cardWidth) .width(cardWidth)
@@ -186,9 +137,9 @@ fun TvContinueWatchingCard(
.background(scheme.surfaceVariant) .background(scheme.surfaceVariant)
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = imageRequest, model = imageUrl,
contentDescription = null, contentDescription = null,
modifier = imageFocusModifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.onFocusChanged { isFocused = it.isFocused } .onFocusChanged { isFocused = it.isFocused }
.clickable { .clickable {
@@ -227,10 +178,6 @@ fun TvContinueWatchingCard(
@Composable @Composable
fun TvNextUpSection( fun TvNextUpSection(
items: List<NextUpItem>, items: List<NextUpItem>,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -241,9 +188,7 @@ fun TvNextUpSection(
) )
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -253,11 +198,8 @@ fun TvNextUpSection(
) { index, item -> ) { index, item ->
TvNextUpCard( TvNextUpCard(
item = item, item = item,
focusRequester = if (index == 0) firstItemFocusRequester else null,
isFirstItem = index == 0, isFirstItem = index == 0,
isLastItem = index == items.lastIndex, isLastItem = index == items.lastIndex,
upFocusRequester = upFocusRequester,
downFocusRequester = downFocusRequester,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected
) )
} }
@@ -268,61 +210,24 @@ fun TvNextUpSection(
fun TvNextUpCard( fun TvNextUpCard(
item: NextUpItem, item: NextUpItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
isFirstItem: Boolean = false, isFirstItem: Boolean = false,
isLastItem: Boolean = false, isLastItem: Boolean = false,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val density = LocalDensity.current
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale") val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
val imageUrl = item.episode.heroImageUrl val imageUrl = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY)
val cardWidth = 280.dp val cardWidth = 280.dp
val cardHeight = cardWidth * 9 / 16
fun openItem(item: NextUpItem) { fun openItem(item: NextUpItem) {
val episode = item.episode val episode = item.episode
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
} }
val imageRequest = ImageRequest.Builder(context)
.data(imageUrl)
.size(with(density) { cardWidth.roundToPx() }, with(density) { cardHeight.roundToPx() })
.build()
val imageFocusModifier = Modifier
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.then(
if (upFocusRequester != null || downFocusRequester != null) {
Modifier.focusProperties {
upFocusRequester?.let { up = it }
downFocusRequester?.let { down = it }
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
} else {
Modifier.focusProperties {
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
}
)
Column( Column(
modifier = modifier modifier = modifier
.width(cardWidth) .width(cardWidth)
@@ -346,9 +251,9 @@ fun TvNextUpCard(
.background(scheme.surfaceVariant) .background(scheme.surfaceVariant)
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = imageRequest, model = imageUrl,
contentDescription = null, contentDescription = null,
modifier = imageFocusModifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.onFocusChanged { isFocused = it.isFocused } .onFocusChanged { isFocused = it.isFocused }
.clickable { .clickable {
@@ -382,10 +287,6 @@ fun TvLibraryPosterSection(
title: String, title: String,
items: List<PosterItem>, items: List<PosterItem>,
action: String?, action: String?,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit,
@@ -397,9 +298,7 @@ fun TvLibraryPosterSection(
) )
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -409,9 +308,6 @@ fun TvLibraryPosterSection(
) { index, item -> ) { index, item ->
PosterCard( PosterCard(
item = item, item = item,
focusRequester = if (index == 0) firstItemFocusRequester else null,
upFocusRequester = upFocusRequester,
downFocusRequester = downFocusRequester,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected, onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected

View File

@@ -1,12 +1,7 @@
package hu.bbara.purefin.tv.home.ui package hu.bbara.purefin.tv.home.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -17,7 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -30,45 +24,28 @@ fun TvHomeTopBar(
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, tabs.lastIndex.coerceAtLeast(0)) val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, tabs.lastIndex.coerceAtLeast(0))
Box( PrimaryScrollableTabRow(
modifier = modifier selectedTabIndex = safeSelectedTabIndex,
.fillMaxWidth() containerColor = scheme.surface,
.background(scheme.background.copy(alpha = 0.95f)) contentColor = scheme.onSurface,
.zIndex(1f)
) { ) {
Row( tabs.forEachIndexed { index, tab ->
modifier = Modifier Tab(
.statusBarsPadding() selected = index == safeSelectedTabIndex,
.padding(horizontal = 16.dp, vertical = 16.dp) onClick = { onTabSelected(index, tab) },
.fillMaxWidth(), text = {
verticalAlignment = Alignment.CenterVertically, Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
) { verticalAlignment = Alignment.CenterVertically
PrimaryScrollableTabRow( ) {
selectedTabIndex = safeSelectedTabIndex, Icon(
modifier = Modifier.weight(1f), imageVector = tab.icon,
containerColor = scheme.surface, contentDescription = null
contentColor = scheme.onSurface )
) { Text(text = tab.label)
tabs.forEachIndexed { index, tab -> }
Tab(
selected = index == safeSelectedTabIndex,
onClick = { onTabSelected(index, tab) },
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = tab.icon,
contentDescription = null
)
Text(text = tab.label)
}
}
)
} }
} )
} }
} }
} }

View File

@@ -7,13 +7,13 @@ import hu.bbara.purefin.app.content.series.SeriesScreen
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.login.ui.LoginScreen import hu.bbara.purefin.login.ui.LoginScreen
import hu.bbara.purefin.tv.home.TvHomeScreen import hu.bbara.purefin.tv.TvAppScreen
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen import hu.bbara.purefin.tv.library.ui.TvLibraryScreen
import hu.bbara.purefin.tv.player.TvPlayerScreen import hu.bbara.purefin.tv.player.TvPlayerScreen
fun EntryProviderScope<Route>.tvHomeSection() { fun EntryProviderScope<Route>.tvHomeSection() {
entry<Route.Home> { entry<Route.Home> {
TvHomeScreen() TvAppScreen()
} }
} }

View File

@@ -5,218 +5,24 @@ import androidx.compose.ui.graphics.Color
private fun hslColor(hue: Float, saturation: Float, lightness: Float): Color = private fun hslColor(hue: Float, saturation: Float, lightness: Float): Color =
Color.hsl(hue = hue, saturation = saturation / 100f, lightness = lightness / 100f) Color.hsl(hue = hue, saturation = saturation / 100f, lightness = lightness / 100f)
val primaryLight = hslColor(26.9f, 61.4f, 33.5f) val primaryDark = hslColor(255f, 70f, 70f)
val onPrimaryLight = hslColor(0f, 0f, 100f) val onPrimaryDark = hslColor(255f, 60f, 12f)
val primaryContainerLight = hslColor(23.8f, 100f, 88.6f) val primaryContainerDark = hslColor(255f, 70f, 70f)
val onPrimaryContainerLight = hslColor(28.2f, 81.7f, 23.5f) val onPrimaryContainerDark = hslColor(255f, 60f, 12f)
val secondaryLight = hslColor(25f, 25.8f, 36.5f) val secondaryDark = hslColor(255f, 40f, 40f)
val onSecondaryLight = hslColor(0f, 0f, 100f) val onSecondaryDark = hslColor(255f, 40f, 88f)
val secondaryContainerLight = hslColor(23.8f, 100f, 88.6f) val secondaryContainerDark = hslColor(255f, 40f, 40f)
val onSecondaryContainerLight = hslColor(24.5f, 31.9f, 27.1f) val onSecondaryContainerDark = hslColor(255f, 40f, 88f)
val tertiaryLight = hslColor(64.1f, 29.3f, 29.4f) val tertiaryDark = hslColor(255f, 15f, 65f)
val onTertiaryLight = hslColor(0f, 0f, 100f) val onTertiaryDark = hslColor(255f, 15f, 12f)
val tertiaryContainerLight = hslColor(62.1f, 52.8f, 79.2f) val tertiaryContainerDark = hslColor(255f, 15f, 65.0f)
val onTertiaryContainerLight = hslColor(64.4f, 39.0f, 20.6f) val onTertiaryContainerDark = hslColor(255f, 15f, 12f)
val errorLight = hslColor(0f, 75.5f, 41.6f) val errorDark = hslColor(7f, 100f, 84f)
val onErrorLight = hslColor(0f, 0f, 100f) val onErrorDark = hslColor(357f, 100f, 21f)
val errorContainerLight = hslColor(5.9f, 100f, 92.0f) val errorContainerDark = hslColor(355f, 100f, 29f)
val onErrorContainerLight = hslColor(355.9f, 100f, 28.8f) val onErrorContainerDark = hslColor(6f, 100f, 92f)
val backgroundLight = hslColor(18f, 100f, 98.0f) val backgroundDark = hslColor(255f, 60f, 8f)
val onBackgroundLight = hslColor(23.1f, 23.6f, 10.8f) val onBackgroundDark = hslColor(255f, 60f, 94f)
val surfaceLight = hslColor(18f, 100f, 98.0f) val surfaceDark = hslColor(255f, 40f, 12f)
val onSurfaceLight = hslColor(23.1f, 23.6f, 10.8f) val onSurfaceDark = hslColor(255f, 40f, 92f)
val surfaceVariantLight = hslColor(23.6f, 57.9f, 88.8f) val surfaceContainerDark = hslColor(255f, 40f, 12f)
val onSurfaceVariantLight = hslColor(23.5f, 16.3f, 27.6f)
val outlineLight = hslColor(23.1f, 10.9f, 46.7f)
val outlineVariantLight = hslColor(23.2f, 27.4f, 77.8f)
val scrimLight = hslColor(0f, 0f, 0f)
val inverseSurfaceLight = hslColor(24f, 15.5f, 19.0f)
val inverseOnSurfaceLight = hslColor(23.1f, 92.9f, 94.5f)
val inversePrimaryLight = hslColor(25.2f, 100f, 75.7f)
val surfaceDimLight = hslColor(21.6f, 34.2f, 85.7f)
val surfaceBrightLight = hslColor(18f, 100f, 98.0f)
val surfaceContainerLowestLight = hslColor(0f, 0f, 100f)
val surfaceContainerLowLight = hslColor(21.8f, 100f, 95.7f)
val surfaceContainerLight = hslColor(23.1f, 76.5f, 93.3f)
val surfaceContainerHighLight = hslColor(21.6f, 55.6f, 91.2f)
val surfaceContainerHighestLight = hslColor(20.8f, 46.4f, 89.0f)
val primaryLightMediumContrast = hslColor(29.0f, 100f, 17.1f)
val onPrimaryLightMediumContrast = hslColor(0f, 0f, 100f)
val primaryContainerLightMediumContrast = hslColor(26.4f, 54.2f, 39.4f)
val onPrimaryContainerLightMediumContrast = hslColor(0f, 0f, 100f)
val secondaryLightMediumContrast = hslColor(24.9f, 39.0f, 20.6f)
val onSecondaryLightMediumContrast = hslColor(0f, 0f, 100f)
val secondaryContainerLightMediumContrast = hslColor(24f, 23.1f, 42.4f)
val onSecondaryContainerLightMediumContrast = hslColor(0f, 0f, 100f)
val tertiaryLightMediumContrast = hslColor(64.5f, 54.1f, 14.5f)
val onTertiaryLightMediumContrast = hslColor(0f, 0f, 100f)
val tertiaryContainerLightMediumContrast = hslColor(63.9f, 25.8f, 34.9f)
val onTertiaryContainerLightMediumContrast = hslColor(0f, 0f, 100f)
val errorLightMediumContrast = hslColor(356.9f, 100f, 22.7f)
val onErrorLightMediumContrast = hslColor(0f, 0f, 100f)
val errorContainerLightMediumContrast = hslColor(1.8f, 68.3f, 48.2f)
val onErrorContainerLightMediumContrast = hslColor(0f, 0f, 100f)
val backgroundLightMediumContrast = hslColor(18f, 100f, 98.0f)
val onBackgroundLightMediumContrast = hslColor(23.1f, 23.6f, 10.8f)
val surfaceLightMediumContrast = hslColor(18f, 100f, 98.0f)
val onSurfaceLightMediumContrast = hslColor(25f, 35.3f, 6.7f)
val surfaceVariantLightMediumContrast = hslColor(23.6f, 57.9f, 88.8f)
val onSurfaceVariantLightMediumContrast = hslColor(25.7f, 19.6f, 21.0f)
val outlineLightMediumContrast = hslColor(25f, 14.6f, 32.2f)
val outlineVariantLightMediumContrast = hslColor(23.1f, 11.9f, 42.7f)
val scrimLightMediumContrast = hslColor(0f, 0f, 0f)
val inverseSurfaceLightMediumContrast = hslColor(24f, 15.5f, 19.0f)
val inverseOnSurfaceLightMediumContrast = hslColor(23.1f, 92.9f, 94.5f)
val inversePrimaryLightMediumContrast = hslColor(25.2f, 100f, 75.7f)
val surfaceDimLightMediumContrast = hslColor(22.5f, 21.4f, 78.0f)
val surfaceBrightLightMediumContrast = hslColor(18f, 100f, 98.0f)
val surfaceContainerLowestLightMediumContrast = hslColor(0f, 0f, 100f)
val surfaceContainerLowLightMediumContrast = hslColor(21.8f, 100f, 95.7f)
val surfaceContainerLightMediumContrast = hslColor(21.6f, 55.6f, 91.2f)
val surfaceContainerHighLightMediumContrast = hslColor(21.6f, 37.3f, 86.9f)
val surfaceContainerHighestLightMediumContrast = hslColor(22.5f, 26.7f, 82.4f)
val primaryLightHighContrast = hslColor(27.9f, 100f, 14.3f)
val onPrimaryLightHighContrast = hslColor(0f, 0f, 100f)
val primaryContainerLightHighContrast = hslColor(28.5f, 79.2f, 24.5f)
val onPrimaryContainerLightHighContrast = hslColor(0f, 0f, 100f)
val secondaryLightHighContrast = hslColor(25.3f, 45.2f, 16.5f)
val onSecondaryLightHighContrast = hslColor(0f, 0f, 100f)
val secondaryContainerLightHighContrast = hslColor(25.9f, 31.0f, 27.8f)
val onSecondaryContainerLightHighContrast = hslColor(0f, 0f, 100f)
val tertiaryLightHighContrast = hslColor(63.1f, 73.6f, 10.4f)
val onTertiaryLightHighContrast = hslColor(0f, 0f, 100f)
val tertiaryContainerLightHighContrast = hslColor(64.3f, 38.2f, 21.6f)
val onTertiaryContainerLightHighContrast = hslColor(0f, 0f, 100f)
val errorLightHighContrast = hslColor(357.5f, 100f, 18.8f)
val onErrorLightHighContrast = hslColor(0f, 0f, 100f)
val errorContainerLightHighContrast = hslColor(356.1f, 100f, 29.8f)
val onErrorContainerLightHighContrast = hslColor(0f, 0f, 100f)
val backgroundLightHighContrast = hslColor(18f, 100f, 98.0f)
val onBackgroundLightHighContrast = hslColor(23.1f, 23.6f, 10.8f)
val surfaceLightHighContrast = hslColor(18f, 100f, 98.0f)
val onSurfaceLightHighContrast = hslColor(0f, 0f, 0f)
val surfaceVariantLightHighContrast = hslColor(23.6f, 57.9f, 88.8f)
val onSurfaceVariantLightHighContrast = hslColor(0f, 0f, 0f)
val outlineLightHighContrast = hslColor(24f, 22.7f, 17.3f)
val outlineVariantLightHighContrast = hslColor(21.8f, 15.1f, 28.6f)
val scrimLightHighContrast = hslColor(0f, 0f, 0f)
val inverseSurfaceLightHighContrast = hslColor(24f, 15.5f, 19.0f)
val inverseOnSurfaceLightHighContrast = hslColor(0f, 0f, 100f)
val inversePrimaryLightHighContrast = hslColor(25.2f, 100f, 75.7f)
val surfaceDimLightHighContrast = hslColor(22.5f, 17.1f, 72.5f)
val surfaceBrightLightHighContrast = hslColor(18f, 100f, 98.0f)
val surfaceContainerLowestLightHighContrast = hslColor(0f, 0f, 100f)
val surfaceContainerLowLightHighContrast = hslColor(23.1f, 92.9f, 94.5f)
val surfaceContainerLightHighContrast = hslColor(20.8f, 46.4f, 89.0f)
val surfaceContainerHighLightHighContrast = hslColor(21.6f, 29.4f, 83.3f)
val surfaceContainerHighestLightHighContrast = hslColor(22.5f, 21.4f, 78.0f)
val primaryDark = hslColor(25.2f, 100f, 75.7f)
val onPrimaryDark = hslColor(28.1f, 100f, 15.5f)
val primaryContainerDark = hslColor(28.2f, 81.7f, 23.5f)
val onPrimaryContainerDark = hslColor(23.8f, 100f, 88.6f)
val secondaryDark = hslColor(23.6f, 53.0f, 77.5f)
val onSecondaryDark = hslColor(24.6f, 41.9f, 18.2f)
val secondaryContainerDark = hslColor(24.5f, 31.9f, 27.1f)
val onSecondaryContainerDark = hslColor(23.8f, 100f, 88.6f)
val tertiaryDark = hslColor(63.3f, 33.7f, 68.6f)
val onTertiaryDark = hslColor(64.5f, 64.5f, 12.2f)
val tertiaryContainerDark = hslColor(64.4f, 39.0f, 20.6f)
val onTertiaryContainerDark = hslColor(62.1f, 52.8f, 79.2f)
val errorDark = hslColor(6.4f, 100f, 83.5f)
val onErrorDark = hslColor(357.1f, 100f, 20.6f)
val errorContainerDark = hslColor(355.9f, 100f, 28.8f)
val onErrorContainerDark = hslColor(5.9f, 100f, 92.0f)
val backgroundDark = hslColor(25f, 31.6f, 7.5f)
val onBackgroundDark = hslColor(20.8f, 46.4f, 89.0f)
val surfaceDark = hslColor(25f, 31.6f, 7.5f)
val onSurfaceDark = hslColor(20.8f, 46.4f, 89.0f)
val surfaceVariantDark = hslColor(23.5f, 16.3f, 27.6f)
val onSurfaceVariantDark = hslColor(23.2f, 27.4f, 77.8f)
val outlineDark = hslColor(21.4f, 12.7f, 56.9f)
val outlineVariantDark = hslColor(23.5f, 16.3f, 27.6f)
val scrimDark = hslColor(0f, 0f, 0f)
val inverseSurfaceDark = hslColor(20.8f, 46.4f, 89.0f)
val inverseOnSurfaceDark = hslColor(24f, 15.5f, 19.0f)
val inversePrimaryDark = hslColor(26.9f, 61.4f, 33.5f)
val surfaceDimDark = hslColor(25f, 31.6f, 7.5f)
val surfaceBrightDark = hslColor(22.5f, 14.0f, 22.4f)
val surfaceContainerLowestDark = hslColor(25f, 42.9f, 5.5f)
val surfaceContainerLowDark = hslColor(23.1f, 23.6f, 10.8f)
val surfaceContainerDark = hslColor(25.7f, 22.6f, 12.2f)
val surfaceContainerHighDark = hslColor(24f, 18.1f, 16.3f)
val surfaceContainerHighestDark = hslColor(24f, 14.3f, 20.6f)
val primaryDarkMediumContrast = hslColor(24.2f, 100f, 85.9f)
val onPrimaryDarkMediumContrast = hslColor(26.7f, 100f, 12.4f)
val primaryContainerDarkMediumContrast = hslColor(25.7f, 50.6f, 53.9f)
val onPrimaryContainerDarkMediumContrast = hslColor(0f, 0f, 0f)
val secondaryDarkMediumContrast = hslColor(23.8f, 88.7f, 86.1f)
val onSecondaryDarkMediumContrast = hslColor(25.9f, 52.1f, 13.9f)
val secondaryContainerDarkMediumContrast = hslColor(24f, 24.7f, 56.3f)
val onSecondaryContainerDarkMediumContrast = hslColor(0f, 0f, 0f)
val tertiaryDarkMediumContrast = hslColor(63.3f, 47.0f, 77.1f)
val onTertiaryDarkMediumContrast = hslColor(64.9f, 86.0f, 8.4f)
val tertiaryContainerDarkMediumContrast = hslColor(63.7f, 19.8f, 48.4f)
val onTertiaryContainerDarkMediumContrast = hslColor(0f, 0f, 0f)
val errorDarkMediumContrast = hslColor(7.1f, 100f, 90f)
val onErrorDarkMediumContrast = hslColor(357.9f, 100f, 16.5f)
val errorContainerDarkMediumContrast = hslColor(3.6f, 100f, 64.3f)
val onErrorContainerDarkMediumContrast = hslColor(0f, 0f, 0f)
val backgroundDarkMediumContrast = hslColor(25f, 31.6f, 7.5f)
val onBackgroundDarkMediumContrast = hslColor(20.8f, 46.4f, 89.0f)
val surfaceDarkMediumContrast = hslColor(25f, 31.6f, 7.5f)
val onSurfaceDarkMediumContrast = hslColor(0f, 0f, 100f)
val surfaceVariantDarkMediumContrast = hslColor(23.5f, 16.3f, 27.6f)
val onSurfaceVariantDarkMediumContrast = hslColor(21.8f, 47.8f, 86.5f)
val outlineDarkMediumContrast = hslColor(22f, 19.5f, 69.8f)
val outlineVariantDarkMediumContrast = hslColor(23.6f, 12.6f, 56.5f)
val scrimDarkMediumContrast = hslColor(0f, 0f, 0f)
val inverseSurfaceDarkMediumContrast = hslColor(20.8f, 46.4f, 89.0f)
val inverseOnSurfaceDarkMediumContrast = hslColor(24f, 18.1f, 16.3f)
val inversePrimaryDarkMediumContrast = hslColor(27.9f, 80.5f, 24.1f)
val surfaceDimDarkMediumContrast = hslColor(25f, 31.6f, 7.5f)
val surfaceBrightDarkMediumContrast = hslColor(21.2f, 12.4f, 26.9f)
val surfaceContainerLowestDarkMediumContrast = hslColor(20f, 60f, 2.9f)
val surfaceContainerLowDarkMediumContrast = hslColor(25.7f, 24.1f, 11.4f)
val surfaceContainerDarkMediumContrast = hslColor(24f, 19.0f, 15.5f)
val surfaceContainerHighDarkMediumContrast = hslColor(24f, 14.9f, 19.8f)
val surfaceContainerHighestDarkMediumContrast = hslColor(24.7f, 13.8f, 24.1f)
val primaryDarkHighContrast = hslColor(20.7f, 100f, 94.3f)
val onPrimaryDarkHighContrast = hslColor(0f, 0f, 0f)
val primaryContainerDarkHighContrast = hslColor(25.5f, 98.5f, 73.7f)
val onPrimaryContainerDarkHighContrast = hslColor(18.3f, 100f, 4.5f)
val secondaryDarkHighContrast = hslColor(20.7f, 100f, 94.3f)
val onSecondaryDarkHighContrast = hslColor(0f, 0f, 0f)
val secondaryContainerDarkHighContrast = hslColor(23.6f, 49.6f, 75.9f)
val onSecondaryContainerDarkHighContrast = hslColor(18.3f, 100f, 4.5f)
val tertiaryDarkHighContrast = hslColor(63.2f, 72.2f, 84.5f)
val onTertiaryDarkHighContrast = hslColor(0f, 0f, 0f)
val tertiaryContainerDarkHighContrast = hslColor(63.4f, 31.7f, 67.3f)
val onTertiaryContainerDarkHighContrast = hslColor(65f, 100f, 2.4f)
val errorDarkHighContrast = hslColor(8.2f, 100f, 95.7f)
val onErrorDarkHighContrast = hslColor(0f, 0f, 0f)
val errorContainerDarkHighContrast = hslColor(6.6f, 100f, 82.2f)
val onErrorContainerDarkHighContrast = hslColor(358.2f, 100f, 6.7f)
val backgroundDarkHighContrast = hslColor(25f, 31.6f, 7.5f)
val onBackgroundDarkHighContrast = hslColor(20.8f, 46.4f, 89.0f)
val surfaceDarkHighContrast = hslColor(25f, 31.6f, 7.5f)
val onSurfaceDarkHighContrast = hslColor(0f, 0f, 100f)
val surfaceVariantDarkHighContrast = hslColor(23.5f, 16.3f, 27.6f)
val onSurfaceVariantDarkHighContrast = hslColor(0f, 0f, 100f)
val outlineDarkHighContrast = hslColor(20.7f, 100f, 94.3f)
val outlineVariantDarkHighContrast = hslColor(23.2f, 25.6f, 76.3f)
val scrimDarkHighContrast = hslColor(0f, 0f, 0f)
val inverseSurfaceDarkHighContrast = hslColor(20.8f, 46.4f, 89.0f)
val inverseOnSurfaceDarkHighContrast = hslColor(0f, 0f, 0f)
val inversePrimaryDarkHighContrast = hslColor(27.9f, 80.5f, 24.1f)
val surfaceDimDarkHighContrast = hslColor(25f, 31.6f, 7.5f)
val surfaceBrightDarkHighContrast = hslColor(23.3f, 11.2f, 31.4f)
val surfaceContainerLowestDarkHighContrast = hslColor(0f, 0f, 0f)
val surfaceContainerLowDarkHighContrast = hslColor(25.7f, 22.6f, 12.2f)
val surfaceContainerDarkHighContrast = hslColor(24f, 15.5f, 19.0f)
val surfaceContainerHighDarkHighContrast = hslColor(22.5f, 13.6f, 23.1f)
val surfaceContainerHighestDarkHighContrast = hslColor(24.7f, 12.1f, 27.6f)

View File

@@ -5,50 +5,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme( private val darkScheme = darkColorScheme(
primary = primaryDark, primary = primaryDark,
onPrimary = onPrimaryDark, onPrimary = onPrimaryDark,
@@ -70,173 +30,7 @@ private val darkScheme = darkColorScheme(
onBackground = onBackgroundDark, onBackground = onBackgroundDark,
surface = surfaceDark, surface = surfaceDark,
onSurface = onSurfaceDark, onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark, surfaceContainer = surfaceContainerDark
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
private val mediumContrastLightColorScheme = lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
private val highContrastLightColorScheme = lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
private val mediumContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
private val highContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
) )
@Immutable @Immutable
@@ -247,10 +41,6 @@ data class ColorFamily(
val onColorContainer: Color val onColorContainer: Color
) )
val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
)
@Composable @Composable
fun AppTheme( fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
@@ -264,7 +54,7 @@ fun AppTheme(
} }
darkTheme -> darkScheme darkTheme -> darkScheme
else -> lightScheme else -> darkScheme
} }
MaterialTheme( MaterialTheme(

View File

@@ -37,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
@@ -45,6 +46,7 @@ import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -162,7 +164,7 @@ private fun EpisodeHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = episode.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
backgroundColor = scheme.background, backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@@ -258,7 +260,7 @@ private fun previewEpisode(): Episode {
progress = 63.0, progress = 63.0,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee", imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
cast = listOf( cast = listOf(
CastMember("Adam Scott", "Mark Scout", null), CastMember("Adam Scott", "Mark Scout", null),
CastMember("Britt Lower", "Helly R.", null), CastMember("Britt Lower", "Helly R.", null),

View File

@@ -35,12 +35,14 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.MovieDto import hu.bbara.purefin.core.data.navigation.MovieDto
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -135,7 +137,7 @@ fun MediaHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = movie.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
backgroundColor = MaterialTheme.colorScheme.background, backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@@ -209,7 +211,7 @@ private fun previewMovie(): Movie =
runtime = "2h 44m", runtime = "2h 44m",
format = "Dolby Vision", format = "Dolby Vision",
synopsis = "A new blade runner uncovers a buried secret that forces him to trace the vanished footsteps of Rick Deckard.", synopsis = "A new blade runner uncovers a buried secret that forces him to trace the vanished footsteps of Rick Deckard.",
heroImageUrl = "https://images.unsplash.com/photo-1519608487953-e999c86e7455", imageUrlPrefix = "https://images.unsplash.com/photo-1519608487953-e999c86e7455",
audioTrack = "English 5.1", audioTrack = "English 5.1",
subtitles = "English CC", subtitles = "English CC",
cast = listOf( cast = listOf(

View File

@@ -65,6 +65,7 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.common.ui.components.MediaResumeButton
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
@@ -75,6 +76,7 @@ import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
internal fun SeriesTopBar( internal fun SeriesTopBar(
@@ -347,7 +349,7 @@ private fun EpisodeCard(
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp)) .border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = episode.heroImageUrl, model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -33,6 +33,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.MediaSynopsis
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.SeriesDto import hu.bbara.purefin.core.data.navigation.SeriesDto
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
@@ -41,6 +42,7 @@ import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -198,7 +200,7 @@ private fun SeriesHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = series.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
backgroundColor = scheme.background, backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@@ -270,7 +272,7 @@ private fun previewSeries(): Series {
progress = 100.0, progress = 100.0,
watched = true, watched = true,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae", imageUrlPrefix = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae",
cast = emptyList() cast = emptyList()
), ),
Episode( Episode(
@@ -286,7 +288,7 @@ private fun previewSeries(): Series {
progress = 34.0, progress = 34.0,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a", imageUrlPrefix = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
cast = emptyList() cast = emptyList()
) )
) )
@@ -304,7 +306,7 @@ private fun previewSeries(): Series {
progress = null, progress = null,
watched = false, watched = false,
format = "4K", format = "4K",
heroImageUrl = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429", imageUrlPrefix = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
cast = emptyList() cast = emptyList()
) )
) )
@@ -315,7 +317,7 @@ private fun previewSeries(): Series {
name = "Constellation", name = "Constellation",
synopsis = "When an experiment in orbit goes wrong, the survivors return home to a world that no longer fits their memories.", synopsis = "When an experiment in orbit goes wrong, the survivors return home to a world that no longer fits their memories.",
year = "2024", year = "2024",
heroImageUrl = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa", imageUrlPrefix = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa",
unwatchedEpisodeCount = 2, unwatchedEpisodeCount = 2,
seasonCount = 2, seasonCount = 2,
seasons = listOf( seasons = listOf(

View File

@@ -21,26 +21,19 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.app.home.ui.continuewatching.ContinueWatchingSection import hu.bbara.purefin.app.home.ui.continuewatching.ContinueWatchingSection
import hu.bbara.purefin.app.home.ui.featured.SuggestionsSection import hu.bbara.purefin.app.home.ui.featured.SuggestionsSection
import hu.bbara.purefin.app.home.ui.library.LibraryPosterSection import hu.bbara.purefin.app.home.ui.library.LibraryPosterSection
import hu.bbara.purefin.app.home.ui.nextup.NextUpSection import hu.bbara.purefin.app.home.ui.nextup.NextUpSection
import hu.bbara.purefin.app.home.ui.shared.HomeEmptyState import hu.bbara.purefin.app.home.ui.shared.HomeEmptyState
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.PosterItem
import hu.bbara.purefin.feature.shared.home.SuggestedItem import hu.bbara.purefin.feature.shared.home.SuggestedItem
import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import java.util.UUID as JavaUuid
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -169,266 +162,4 @@ fun HomeContent(
} }
} }
} }
} }
@Preview(name = "Home Full", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeContentPreview() {
AppTheme(darkTheme = true) {
HomeContent(
libraries = homePreviewLibraries(),
libraryContent = homePreviewLibraryContent(),
suggestions = emptyList(),
continueWatching = homePreviewContinueWatching(),
nextUp = homePreviewNextUp(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onBrowseLibrariesClick = {}
)
}
}
@Preview(name = "Home Libraries Only", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeLibrariesOnlyPreview() {
AppTheme(darkTheme = true) {
HomeContent(
libraries = homePreviewLibraries(),
libraryContent = homePreviewLibraryContent(),
suggestions = emptyList(),
continueWatching = emptyList(),
nextUp = emptyList(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onBrowseLibrariesClick = {}
)
}
}
@Preview(name = "Home Empty", showBackground = true, widthDp = 412, heightDp = 915)
@Composable
private fun HomeEmptyPreview() {
AppTheme(darkTheme = false) {
HomeContent(
libraries = emptyList(),
libraryContent = emptyMap(),
suggestions = emptyList(),
continueWatching = emptyList(),
nextUp = emptyList(),
isRefreshing = false,
onRefresh = {},
onMovieSelected = {},
onSeriesSelected = {},
onEpisodeSelected = { _, _, _ -> },
onLibrarySelected = {},
onBrowseLibrariesClick = {}
)
}
}
internal fun homePreviewLibraries(): List<LibraryItem> {
return listOf(
LibraryItem(
id = JavaUuid.fromString("11111111-1111-1111-1111-111111111111"),
name = "Movies",
type = CollectionType.MOVIES,
posterUrl = "https://images.unsplash.com/photo-1517604931442-7e0c8ed2963c",
isEmpty = false
),
LibraryItem(
id = JavaUuid.fromString("22222222-2222-2222-2222-222222222222"),
name = "Series",
type = CollectionType.TVSHOWS,
posterUrl = "https://images.unsplash.com/photo-1489599849927-2ee91cede3ba",
isEmpty = false
)
)
}
internal fun homePreviewLibraryContent(): Map<UUID, List<PosterItem>> {
val movie = homePreviewMovie(
id = "33333333-3333-3333-3333-333333333333",
title = "Blade Runner 2049",
year = "2017",
runtime = "2h 44m",
rating = "16+",
format = "Dolby Vision",
synopsis = "A young blade runner uncovers a buried secret that pulls him toward a vanished legend.",
heroImageUrl = "https://images.unsplash.com/photo-1519608487953-e999c86e7455",
progress = 42.0,
watched = false
)
val secondMovie = homePreviewMovie(
id = "44444444-4444-4444-4444-444444444444",
title = "Arrival",
year = "2016",
runtime = "1h 56m",
rating = "12+",
format = "4K",
synopsis = "A linguist is recruited when mysterious spacecraft touch down around the world.",
heroImageUrl = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
progress = null,
watched = false
)
val series = homePreviewSeries()
val episode = homePreviewEpisode(
id = "66666666-6666-6666-6666-666666666666",
title = "Signals",
index = 2,
releaseDate = "2025",
runtime = "48m",
rating = "16+",
progress = 18.0,
watched = false,
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
synopsis = "Anomalies around the station point to a cover-up."
)
return mapOf(
homePreviewLibraries()[0].id to listOf(
PosterItem(type = BaseItemKind.MOVIE, movie = movie),
PosterItem(type = BaseItemKind.MOVIE, movie = secondMovie)
),
homePreviewLibraries()[1].id to listOf(
PosterItem(type = BaseItemKind.SERIES, series = series),
PosterItem(type = BaseItemKind.EPISODE, episode = episode)
)
)
}
internal fun homePreviewContinueWatching(): List<ContinueWatchingItem> {
return listOf(
ContinueWatchingItem(
type = BaseItemKind.MOVIE,
movie = homePreviewMovie(
id = "77777777-7777-7777-7777-777777777777",
title = "Dune: Part Two",
year = "2024",
runtime = "2h 46m",
rating = "13+",
format = "IMAX",
synopsis = "Paul Atreides unites with the Fremen while seeking justice and revenge.",
heroImageUrl = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa",
progress = 58.0,
watched = false
)
),
ContinueWatchingItem(
type = BaseItemKind.EPISODE,
episode = homePreviewEpisode(
id = "88888888-8888-8888-8888-888888888888",
title = "A Fresh Start",
index = 1,
releaseDate = "2025",
runtime = "51m",
rating = "16+",
progress = 23.0,
watched = false,
heroImageUrl = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae",
synopsis = "A fractured crew tries to reassemble after a year apart."
)
)
)
}
internal fun homePreviewNextUp(): List<NextUpItem> {
return listOf(
NextUpItem(
episode = homePreviewEpisode(
id = "99999999-9999-9999-9999-999999999999",
title = "Return Window",
index = 3,
releaseDate = "2025",
runtime = "54m",
rating = "16+",
progress = null,
watched = false,
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
synopsis = "A high-risk jump changes the rules of the mission."
)
)
)
}
internal fun homePreviewMovie(
id: String,
title: String,
year: String,
runtime: String,
rating: String,
format: String,
synopsis: String,
heroImageUrl: String,
progress: Double?,
watched: Boolean
): Movie {
return Movie(
id = JavaUuid.fromString(id),
libraryId = JavaUuid.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
title = title,
progress = progress,
watched = watched,
year = year,
rating = rating,
runtime = runtime,
format = format,
synopsis = synopsis,
heroImageUrl = heroImageUrl,
audioTrack = "English 5.1",
subtitles = "English CC",
cast = emptyList()
)
}
internal fun homePreviewSeries(): Series {
return Series(
id = JavaUuid.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
libraryId = JavaUuid.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc"),
name = "Orbital",
synopsis = "A reluctant crew returns to a damaged station as political pressure mounts on Earth.",
year = "2025",
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
unwatchedEpisodeCount = 4,
seasonCount = 2,
seasons = emptyList(),
cast = emptyList()
)
}
internal fun homePreviewEpisode(
id: String,
title: String,
index: Int,
releaseDate: String,
runtime: String,
rating: String,
progress: Double?,
watched: Boolean,
heroImageUrl: String,
synopsis: String
): Episode {
return Episode(
id = JavaUuid.fromString(id),
seriesId = JavaUuid.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd"),
seasonId = JavaUuid.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"),
index = index,
title = title,
synopsis = synopsis,
releaseDate = releaseDate,
rating = rating,
runtime = runtime,
progress = progress,
watched = watched,
format = "4K",
heroImageUrl = heroImageUrl,
cast = emptyList()
)
}

View File

@@ -25,9 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.common.ui.components.MediaProgressBar import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
internal fun ContinueWatchingCard( internal fun ContinueWatchingCard(
@@ -51,8 +53,14 @@ internal fun ContinueWatchingCard(
else -> "" else -> ""
} }
val imageUrl = when (item.type) { val imageUrl = when (item.type) {
BaseItemKind.MOVIE -> item.movie?.heroImageUrl BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
BaseItemKind.EPISODE -> item.episode?.heroImageUrl prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
else -> null else -> null
} }

View File

@@ -23,8 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.NextUpItem
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.ImageType
@Composable @Composable
internal fun NextUpCard( internal fun NextUpCard(
@@ -55,7 +57,7 @@ internal fun NextUpCard(
.background(scheme.surface) .background(scheme.surface)
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = item.episode.heroImageUrl, model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY),
contentDescription = item.primaryText, contentDescription = item.primaryText,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -433,10 +433,9 @@ class InMemoryAppContentRepository @Inject constructor(
runtime = formatRuntime(this.runTimeTicks), runtime = formatRuntime(this.runTimeTicks),
synopsis = this.overview ?: "No synopsis available", synopsis = this.overview ?: "No synopsis available",
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
heroImageUrl = JellyfinImageHelper.toImageUrl( imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = this.id, itemId = this.id,
type = ImageType.PRIMARY
), ),
subtitles = "ENG", subtitles = "ENG",
audioTrack = "ENG", audioTrack = "ENG",
@@ -452,10 +451,9 @@ class InMemoryAppContentRepository @Inject constructor(
synopsis = this.overview ?: "No synopsis available", synopsis = this.overview ?: "No synopsis available",
year = this.productionYear?.toString() year = this.productionYear?.toString()
?: this.premiereDate?.year?.toString().orEmpty(), ?: this.premiereDate?.year?.toString().orEmpty(),
heroImageUrl = JellyfinImageHelper.toImageUrl( imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = this.id, itemId = this.id
type = ImageType.PRIMARY
), ),
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!, unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
seasonCount = this.childCount!!, seasonCount = this.childCount!!,
@@ -478,11 +476,10 @@ class InMemoryAppContentRepository @Inject constructor(
private fun BaseItemDto.toEpisode(serverUrl: String): Episode { private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
val releaseDate = formatReleaseDate(premiereDate, productionYear) val releaseDate = formatReleaseDate(premiereDate, productionYear)
val heroImageUrl = id?.let { itemId -> val imageUrlPrefix = id?.let { itemId ->
JellyfinImageHelper.toImageUrl( JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = itemId, itemId = itemId
type = ImageType.PRIMARY
) )
} ?: "" } ?: ""
return Episode( return Episode(
@@ -498,7 +495,7 @@ class InMemoryAppContentRepository @Inject constructor(
watched = userData!!.played, watched = userData!!.played,
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.", synopsis = overview ?: "No synopsis available.",
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
cast = emptyList() cast = emptyList()
) )
} }

View File

@@ -125,8 +125,8 @@ class InMemoryMediaRepository @Inject constructor(
private fun BaseItemDto.toEpisode(serverUrl: String): Episode { private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
val releaseDate = formatReleaseDate(premiereDate, productionYear) val releaseDate = formatReleaseDate(premiereDate, productionYear)
val heroImageUrl = id?.let { itemId -> val imageUrlPrefix = id?.let { itemId ->
JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = itemId, type = ImageType.PRIMARY) JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = itemId)
} ?: "" } ?: ""
return Episode( return Episode(
id = id, id = id,
@@ -141,7 +141,7 @@ class InMemoryMediaRepository @Inject constructor(
watched = userData!!.played, watched = userData!!.played,
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.", synopsis = overview ?: "No synopsis available.",
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
cast = emptyList() cast = emptyList()
) )
} }

View File

@@ -5,6 +5,7 @@ import org.jellyfin.sdk.model.api.ImageType
class JellyfinImageHelper { class JellyfinImageHelper {
companion object { companion object {
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String { fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
if (url.isEmpty()) { if (url.isEmpty()) {
return "" return ""
@@ -17,5 +18,28 @@ class JellyfinImageHelper {
.append(type.serialName) .append(type.serialName)
.toString() .toString()
} }
fun toPrefixImageUrl(url: String, itemId: UUID): String {
if (url.isEmpty()) {
return ""
}
return StringBuilder()
.append(url)
.append("/Items/")
.append(itemId)
.append("/Images/")
.toString()
}
fun finishImageUrl(prefixImageUrl: String?, imageType: ImageType): String {
if (prefixImageUrl.isNullOrEmpty()) {
return ""
}
return StringBuilder()
.append(prefixImageUrl)
.append(imageType.serialName)
.toString()
}
} }
} }

View File

@@ -31,5 +31,5 @@ data class EpisodeEntity(
val progress: Double?, val progress: Double?,
val watched: Boolean, val watched: Boolean,
val format: String, val format: String,
val heroImageUrl: String val imageUrlPrefix: String
) )

View File

@@ -1,7 +1,6 @@
package hu.bbara.purefin.core.data.room.entity package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.util.UUID import java.util.UUID
@@ -21,7 +20,7 @@ data class MovieEntity(
val runtime: String, val runtime: String,
val format: String, val format: String,
val synopsis: String, val synopsis: String,
val heroImageUrl: String, val imageUrlPrefix: String,
val audioTrack: String, val audioTrack: String,
val subtitles: String val subtitles: String
) )

View File

@@ -1,7 +1,6 @@
package hu.bbara.purefin.core.data.room.entity package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.util.UUID import java.util.UUID
@@ -16,7 +15,7 @@ data class SeriesEntity(
val name: String, val name: String,
val synopsis: String, val synopsis: String,
val year: String, val year: String,
val heroImageUrl: String, val imageUrlPrefix: String,
val unwatchedEpisodeCount: Int, val unwatchedEpisodeCount: Int,
val seasonCount: Int val seasonCount: Int
) )

View File

@@ -23,7 +23,7 @@ import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity
EpisodeEntity::class, EpisodeEntity::class,
SmartDownloadEntity::class, SmartDownloadEntity::class,
], ],
version = 6, version = 9,
exportSchema = false exportSchema = false
) )
@TypeConverters(UuidConverters::class) @TypeConverters(UuidConverters::class)
@@ -33,4 +33,4 @@ abstract class OfflineMediaDatabase : RoomDatabase() {
abstract fun seasonDao(): SeasonDao abstract fun seasonDao(): SeasonDao
abstract fun episodeDao(): EpisodeDao abstract fun episodeDao(): EpisodeDao
abstract fun smartDownloadDao(): SmartDownloadDao abstract fun smartDownloadDao(): SmartDownloadDao
} }

View File

@@ -208,7 +208,7 @@ class OfflineRoomMediaLocalDataSource(
runtime = runtime, runtime = runtime,
format = format, format = format,
synopsis = synopsis, synopsis = synopsis,
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
audioTrack = audioTrack, audioTrack = audioTrack,
subtitles = subtitles subtitles = subtitles
) )
@@ -219,7 +219,7 @@ class OfflineRoomMediaLocalDataSource(
name = name, name = name,
synopsis = synopsis, synopsis = synopsis,
year = year, year = year,
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
unwatchedEpisodeCount = unwatchedEpisodeCount, unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount seasonCount = seasonCount
) )
@@ -246,7 +246,7 @@ class OfflineRoomMediaLocalDataSource(
progress = progress, progress = progress,
watched = watched, watched = watched,
format = format, format = format,
heroImageUrl = heroImageUrl imageUrlPrefix = imageUrlPrefix
) )
private fun MovieEntity.toDomain() = Movie( private fun MovieEntity.toDomain() = Movie(
@@ -260,7 +260,7 @@ class OfflineRoomMediaLocalDataSource(
runtime = runtime, runtime = runtime,
format = format, format = format,
synopsis = synopsis, synopsis = synopsis,
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
audioTrack = audioTrack, audioTrack = audioTrack,
subtitles = subtitles, subtitles = subtitles,
cast = emptyList() cast = emptyList()
@@ -272,7 +272,7 @@ class OfflineRoomMediaLocalDataSource(
name = name, name = name,
synopsis = synopsis, synopsis = synopsis,
year = year, year = year,
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
unwatchedEpisodeCount = unwatchedEpisodeCount, unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount, seasonCount = seasonCount,
seasons = seasons, seasons = seasons,
@@ -302,7 +302,7 @@ class OfflineRoomMediaLocalDataSource(
progress = progress, progress = progress,
watched = watched, watched = watched,
format = format, format = format,
heroImageUrl = heroImageUrl, imageUrlPrefix = imageUrlPrefix,
cast = emptyList() cast = emptyList()
) )
} }

View File

@@ -15,6 +15,6 @@ data class Episode(
val progress: Double?, val progress: Double?,
val watched: Boolean, val watched: Boolean,
val format: String, val format: String,
val heroImageUrl: String, val imageUrlPrefix: String,
val cast: List<CastMember> val cast: List<CastMember>
) )

View File

@@ -13,7 +13,7 @@ data class Movie(
val runtime: String, val runtime: String,
val format: String, val format: String,
val synopsis: String, val synopsis: String,
val heroImageUrl: String, val imageUrlPrefix: String,
val audioTrack: String, val audioTrack: String,
val subtitles: String, val subtitles: String,
val cast: List<CastMember> val cast: List<CastMember>

View File

@@ -8,7 +8,7 @@ data class Series(
val name: String, val name: String,
val synopsis: String, val synopsis: String,
val year: String, val year: String,
val heroImageUrl: String, val imageUrlPrefix: String,
val unwatchedEpisodeCount: Int, val unwatchedEpisodeCount: Int,
val seasonCount: Int, val seasonCount: Int,
val seasons: List<Season>, val seasons: List<Season>,

View File

@@ -124,6 +124,7 @@ class PlayerMediaRepository @Inject constructor(
} }
} }
@OptIn(UnstableApi::class)
private suspend fun buildOfflineMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? { private suspend fun buildOfflineMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? {
val download = downloadManager.downloadIndex.getDownload(mediaId.toString()) val download = downloadManager.downloadIndex.getDownload(mediaId.toString())
?.takeIf { it.state == Download.STATE_COMPLETED } ?.takeIf { it.state == Download.STATE_COMPLETED }
@@ -138,8 +139,8 @@ class PlayerMediaRepository @Inject constructor(
} }
val subtitle = episode?.let { episodeSubtitle(null, it.index) } val subtitle = episode?.let { episodeSubtitle(null, it.index) }
val artworkUrl = when { val artworkUrl = when {
movie != null -> movie.heroImageUrl movie != null -> JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY)
episode != null -> episode.heroImageUrl episode != null -> JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY)
else -> JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY) else -> JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY)
} }
val resumePositionMs = resumePositionFor(movie, episode) val resumePositionMs = resumePositionFor(movie, episode)

View File

@@ -23,7 +23,6 @@ import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,6 +31,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
@@ -156,10 +156,9 @@ class MediaDownloadManager @Inject constructor(
runtime = formatRuntime(itemInfo.runTimeTicks), runtime = formatRuntime(itemInfo.runTimeTicks),
synopsis = itemInfo.overview ?: "No synopsis available", synopsis = itemInfo.overview ?: "No synopsis available",
format = itemInfo.container?.uppercase() ?: "VIDEO", format = itemInfo.container?.uppercase() ?: "VIDEO",
heroImageUrl = JellyfinImageHelper.toImageUrl( imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = itemInfo.id, itemId = itemInfo.id
type = ImageType.PRIMARY
), ),
subtitles = "ENG", subtitles = "ENG",
audioTrack = "ENG", audioTrack = "ENG",
@@ -370,10 +369,9 @@ class MediaDownloadManager @Inject constructor(
watched = userData?.played ?: false, watched = userData?.played ?: false,
format = container?.uppercase() ?: "VIDEO", format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.", synopsis = overview ?: "No synopsis available.",
heroImageUrl = JellyfinImageHelper.toImageUrl( imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = id, itemId = id
type = ImageType.PRIMARY
), ),
cast = emptyList() cast = emptyList()
) )
@@ -386,10 +384,9 @@ class MediaDownloadManager @Inject constructor(
name = name ?: "Unknown", name = name ?: "Unknown",
synopsis = overview ?: "No synopsis available", synopsis = overview ?: "No synopsis available",
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(), year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
heroImageUrl = JellyfinImageHelper.toImageUrl( imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl, url = serverUrl,
itemId = id, itemId = id
type = ImageType.PRIMARY
), ),
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0, unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
seasonCount = childCount ?: 0, seasonCount = childCount ?: 0,

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.data.OfflineMediaRepository import hu.bbara.purefin.core.data.OfflineMediaRepository
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.navigation.MovieDto import hu.bbara.purefin.core.data.navigation.MovieDto
import hu.bbara.purefin.core.data.navigation.NavigationManager import hu.bbara.purefin.core.data.navigation.NavigationManager
import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.Route
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -70,7 +72,7 @@ class DownloadsViewModel @Inject constructor(
contentId = contentId, contentId = contentId,
title = movie.title, title = movie.title,
subtitle = "", subtitle = "",
imageUrl = movie.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
progress = progress progress = progress
) )
} else { } else {
@@ -80,7 +82,7 @@ class DownloadsViewModel @Inject constructor(
contentId = contentId, contentId = contentId,
title = it.title, title = it.title,
subtitle = seriesMap[it.seriesId]?.name ?: "", subtitle = seriesMap[it.seriesId]?.name ?: "",
imageUrl = it.heroImageUrl, imageUrl = JellyfinImageHelper.finishImageUrl(it.imageUrlPrefix, ImageType.PRIMARY),
progress = progress progress = progress
) )
} }

View File

@@ -1,11 +1,13 @@
package hu.bbara.purefin.feature.shared.home package hu.bbara.purefin.feature.shared.home
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ImageType
sealed interface SuggestedItem { sealed interface SuggestedItem {
@@ -36,7 +38,10 @@ data class SuggestedEpisode (
override val id: UUID = episode.id, override val id: UUID = episode.id,
override val title: String = episode.title, override val title: String = episode.title,
override val description: String = episode.synopsis, override val description: String = episode.synopsis,
override val imageUrl: String = episode.heroImageUrl override val imageUrl: String = JellyfinImageHelper.finishImageUrl(
prefixImageUrl = episode.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
) : SuggestedItem ) : SuggestedItem
data class SuggestedSeries ( data class SuggestedSeries (
@@ -56,7 +61,10 @@ data class SuggestedSeries (
override val id: UUID = series.id, override val id: UUID = series.id,
override val title: String = series.name, override val title: String = series.name,
override val description: String = series.synopsis, override val description: String = series.synopsis,
override val imageUrl: String = series.heroImageUrl override val imageUrl: String = JellyfinImageHelper.finishImageUrl(
prefixImageUrl = series.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
) : SuggestedItem ) : SuggestedItem
data class SuggestedMovie ( data class SuggestedMovie (
@@ -74,7 +82,10 @@ data class SuggestedMovie (
override val id: UUID = movie.id, override val id: UUID = movie.id,
override val title: String = movie.title, override val title: String = movie.title,
override val description: String = movie.synopsis, override val description: String = movie.synopsis,
override val imageUrl: String = movie.heroImageUrl override val imageUrl: String = JellyfinImageHelper.finishImageUrl(
prefixImageUrl = movie.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
) : SuggestedItem ) : SuggestedItem
data class ContinueWatchingItem( data class ContinueWatchingItem(
@@ -139,9 +150,18 @@ data class PosterItem(
else -> throw IllegalArgumentException("Invalid type: $type") else -> throw IllegalArgumentException("Invalid type: $type")
} }
val imageUrl: String = when (type) { val imageUrl: String = when (type) {
BaseItemKind.MOVIE -> movie!!.heroImageUrl BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
BaseItemKind.EPISODE -> episode!!.heroImageUrl prefixImageUrl = movie!!.imageUrlPrefix,
BaseItemKind.SERIES -> series!!.heroImageUrl imageType = ImageType.PRIMARY
)
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = episode!!.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
BaseItemKind.SERIES -> JellyfinImageHelper.finishImageUrl(
prefixImageUrl = series!!.imageUrlPrefix,
imageType = ImageType.PRIMARY
)
else -> throw IllegalArgumentException("Invalid type: $type") else -> throw IllegalArgumentException("Invalid type: $type")
} }
fun watched() = when (type) { fun watched() = when (type) {

View File

@@ -16,6 +16,7 @@ datastore = "1.1.1"
kotlinxSerializationJson = "1.7.3" kotlinxSerializationJson = "1.7.3"
okhttp = "4.12.0" okhttp = "4.12.0"
foundation = "1.10.1" foundation = "1.10.1"
tvMaterial = "1.0.1"
coil = "3.3.0" coil = "3.3.0"
media3 = "1.9.0" media3 = "1.9.0"
media3FfmpegDecoder = "1.9.0+1" media3FfmpegDecoder = "1.9.0+1"
@@ -40,6 +41,7 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-tv-material3 = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" }
jellyfin-core = { group = "org.jellyfin.sdk", name = "jellyfin-core", version.ref = "jellyfin-core" } jellyfin-core = { group = "org.jellyfin.sdk", name = "jellyfin-core", version.ref = "jellyfin-core" }
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }