mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Compare commits
12 Commits
0df79f55a1
...
584e430431
| Author | SHA1 | Date | |
|---|---|---|---|
| 584e430431 | |||
| 680df51ce7 | |||
| 98850087ff | |||
| ce5266f17c | |||
| d6d05be93f | |||
| 61ce6abb39 | |||
| 6e4ea692d5 | |||
| de011a5f3b | |||
| b3d3f60d78 | |||
| cdb9dc3403 | |||
| 866830b8f4 | |||
| b90632063c |
@@ -65,6 +65,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.tv.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
@@ -101,7 +101,7 @@ class EpisodeScreenContentTest {
|
||||
progress = progress,
|
||||
watched = false,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
cast = listOf(
|
||||
CastMember("Adam Scott", "Mark Scout", null)
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ class MovieScreenContentTest {
|
||||
runtime = "164m",
|
||||
format = "4K",
|
||||
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",
|
||||
subtitles = "ENG",
|
||||
cast = listOf(
|
||||
|
||||
@@ -92,7 +92,7 @@ class SeriesScreenContentTest {
|
||||
name = "Severance",
|
||||
synopsis = "Mark leads a team of office workers whose memories have been surgically divided.",
|
||||
year = "2022",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
unwatchedEpisodeCount = 3,
|
||||
seasonCount = 1,
|
||||
seasons = listOf(
|
||||
@@ -117,7 +117,7 @@ class SeriesScreenContentTest {
|
||||
progress = 18.0,
|
||||
watched = false,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
cast = emptyList()
|
||||
),
|
||||
Episode(
|
||||
@@ -133,7 +133,7 @@ class SeriesScreenContentTest {
|
||||
progress = null,
|
||||
watched = false,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
cast = emptyList()
|
||||
)
|
||||
)
|
||||
@@ -154,7 +154,7 @@ class SeriesScreenContentTest {
|
||||
name = "Foundation",
|
||||
synopsis = "A band of exiles works to preserve knowledge through the fall of an empire.",
|
||||
year = "2021",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
unwatchedEpisodeCount = 0,
|
||||
seasonCount = 1,
|
||||
seasons = listOf(
|
||||
|
||||
@@ -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.MediaDetailSectionTitle
|
||||
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.LocalNavigationBackStack
|
||||
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
fun EpisodeScreen(
|
||||
@@ -91,7 +93,7 @@ internal fun EpisodeScreenContent(
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = episode.heroImageUrl,
|
||||
heroImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
resetScrollKey = episode.id,
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
|
||||
@@ -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.MediaDetailSectionTitle
|
||||
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.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
fun MovieScreen(
|
||||
@@ -60,7 +62,7 @@ internal fun MovieScreenContent(
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = movie.heroImageUrl,
|
||||
heroImageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
||||
resetScrollKey = movie.id,
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
|
||||
@@ -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.PurefinAsyncImage
|
||||
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.Episode
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
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 SeriesFirstSeasonTabTag = "series-first-season-tab"
|
||||
@@ -310,7 +312,7 @@ private fun EpisodeCard(
|
||||
)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = episode.heroImageUrl,
|
||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -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.MediaDetailSectionTitle
|
||||
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.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
fun SeriesScreen(
|
||||
@@ -78,7 +80,7 @@ internal fun SeriesScreenContent(
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = series.heroImageUrl,
|
||||
heroImageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
|
||||
resetScrollKey = series.id,
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
|
||||
@@ -20,20 +20,14 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.UnwatchedEpisodeIndicator
|
||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
||||
@@ -45,21 +39,15 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
fun PosterCard(
|
||||
item: PosterItem,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester? = null,
|
||||
upFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onSeriesSelected: (UUID) -> Unit,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
||||
|
||||
val posterWidth = 144.dp
|
||||
val posterHeight = posterWidth * 3 / 2
|
||||
|
||||
fun openItem(posterItem: PosterItem) {
|
||||
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(
|
||||
modifier = modifier
|
||||
.width(posterWidth)
|
||||
@@ -102,9 +72,9 @@ fun PosterCard(
|
||||
) {
|
||||
Box() {
|
||||
PurefinAsyncImage(
|
||||
model = imageRequest,
|
||||
model = item.imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = imageFocusModifier
|
||||
modifier = Modifier
|
||||
.aspectRatio(2f / 3f)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.border(
|
||||
|
||||
136
app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt
Normal file
136
app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +1,35 @@
|
||||
package hu.bbara.purefin.tv.home
|
||||
|
||||
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.Tv
|
||||
import androidx.compose.material3.Scaffold
|
||||
import android.annotation.SuppressLint
|
||||
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.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
||||
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
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
|
||||
@SuppressLint("RememberInComposition")
|
||||
@Composable
|
||||
fun TvHomeScreen(
|
||||
viewModel: AppViewModel = hiltViewModel(),
|
||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||
libraries: List<LibraryItem>,
|
||||
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
|
||||
) {
|
||||
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.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,
|
||||
libraryContent = libraryContent,
|
||||
continueWatching = continueWatching,
|
||||
nextUp = nextUp,
|
||||
onMovieSelected = viewModel::onMovieSelected,
|
||||
onSeriesSelected = viewModel::onSeriesSelected,
|
||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
onMovieSelected = onMovieSelected,
|
||||
onSeriesSelected = onSeriesSelected,
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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 hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
@@ -33,35 +36,10 @@ fun TvHomeContent(
|
||||
val visibleLibraries = remember(libraries, libraryContent) {
|
||||
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) {
|
||||
firstSectionFocusRequester?.requestFocus()
|
||||
val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() }
|
||||
val libraryRefs = remember(libraryContent) {
|
||||
libraryContent.keys.associateWith { FocusRequester() }
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
@@ -75,11 +53,12 @@ fun TvHomeContent(
|
||||
item {
|
||||
TvContinueWatchingSection(
|
||||
items = continueWatching,
|
||||
sectionFocusRequester = continueWatchingSectionFocusRequester,
|
||||
firstItemFocusRequester = continueWatchingFirstItemFocusRequester,
|
||||
downFocusRequester = firstHomeRowBelowContinueWatching,
|
||||
onMovieSelected = onMovieSelected,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
modifier = Modifier.focusRequester(continueWatchingRef)
|
||||
.focusProperties {
|
||||
down = nextUpRef
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
@@ -88,12 +67,12 @@ fun TvHomeContent(
|
||||
item {
|
||||
TvNextUpSection(
|
||||
items = nextUp,
|
||||
sectionFocusRequester = nextUpSectionFocusRequester,
|
||||
firstItemFocusRequester = nextUpFirstItemFocusRequester,
|
||||
upFocusRequester = continueWatching.takeIf { it.isNotEmpty() }
|
||||
?.let { continueWatchingSectionFocusRequester },
|
||||
downFocusRequester = firstVisibleLibrarySectionFocusRequester,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
modifier = Modifier.focusRequester(nextUpRef)
|
||||
.focusProperties {
|
||||
up = continueWatchingRef
|
||||
libraryRefs.values.firstOrNull()?.let { down = it }
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
@@ -103,29 +82,20 @@ fun TvHomeContent(
|
||||
items = visibleLibraries,
|
||||
key = { it.id }
|
||||
) { item ->
|
||||
val libraryIndex = visibleLibraries.indexOfFirst { it.id == item.id }
|
||||
val previousLibrarySectionFocusRequester = visibleLibraries
|
||||
.getOrNull(libraryIndex - 1)
|
||||
?.let { librarySectionFocusRequesters[it.id] }
|
||||
val nextLibrarySectionFocusRequester = visibleLibraries
|
||||
.getOrNull(libraryIndex + 1)
|
||||
?.let { librarySectionFocusRequesters[it.id] }
|
||||
|
||||
val ref = libraryRefs[item.id]
|
||||
TvLibraryPosterSection(
|
||||
title = item.name,
|
||||
items = libraryContent[item.id] ?: emptyList(),
|
||||
action = "See All",
|
||||
sectionFocusRequester = librarySectionFocusRequesters.getValue(item.id),
|
||||
firstItemFocusRequester = libraryFirstItemFocusRequesters.getValue(item.id),
|
||||
upFocusRequester = if (libraryIndex == 0) {
|
||||
firstHomeRowAboveLibraries
|
||||
} else {
|
||||
previousLibrarySectionFocusRequester
|
||||
},
|
||||
downFocusRequester = nextLibrarySectionFocusRequester,
|
||||
onMovieSelected = onMovieSelected,
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ data class TvHomeNavItem(
|
||||
enum class TvHomeTabDestination {
|
||||
SEARCH,
|
||||
HOME,
|
||||
LIBRARY
|
||||
LIBRARY,
|
||||
SETTINGS
|
||||
}
|
||||
|
||||
data class TvHomeTabItem(
|
||||
|
||||
@@ -28,38 +28,29 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.focusRestorer
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.request.ImageRequest
|
||||
import hu.bbara.purefin.common.ui.PosterCard
|
||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||
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.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import kotlin.math.nextUp
|
||||
|
||||
@Composable
|
||||
fun TvContinueWatchingSection(
|
||||
items: List<ContinueWatchingItem>,
|
||||
sectionFocusRequester: FocusRequester,
|
||||
firstItemFocusRequester: FocusRequester? = null,
|
||||
upFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -71,20 +62,13 @@ fun TvContinueWatchingSection(
|
||||
)
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(sectionFocusRequester)
|
||||
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
itemsIndexed(items = items) { index, item ->
|
||||
TvContinueWatchingCard(
|
||||
item = item,
|
||||
focusRequester = if (index == 0) firstItemFocusRequester else null,
|
||||
isFirstItem = index == 0,
|
||||
isLastItem = index == items.lastIndex,
|
||||
upFocusRequester = upFocusRequester,
|
||||
downFocusRequester = downFocusRequester,
|
||||
onMovieSelected = onMovieSelected,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
)
|
||||
@@ -96,29 +80,27 @@ fun TvContinueWatchingSection(
|
||||
fun TvContinueWatchingCard(
|
||||
item: ContinueWatchingItem,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester? = null,
|
||||
isFirstItem: Boolean = false,
|
||||
isLastItem: Boolean = false,
|
||||
upFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
|
||||
|
||||
val imageUrl = when (item.type) {
|
||||
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
|
||||
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
|
||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val cardWidth = 280.dp
|
||||
val cardHeight = cardWidth * 9 / 16
|
||||
|
||||
fun openItem(item: ContinueWatchingItem) {
|
||||
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(
|
||||
modifier = modifier
|
||||
.width(cardWidth)
|
||||
@@ -186,9 +137,9 @@ fun TvContinueWatchingCard(
|
||||
.background(scheme.surfaceVariant)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = imageRequest,
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = imageFocusModifier
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable {
|
||||
@@ -227,10 +178,6 @@ fun TvContinueWatchingCard(
|
||||
@Composable
|
||||
fun TvNextUpSection(
|
||||
items: List<NextUpItem>,
|
||||
sectionFocusRequester: FocusRequester,
|
||||
firstItemFocusRequester: FocusRequester? = null,
|
||||
upFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -241,9 +188,7 @@ fun TvNextUpSection(
|
||||
)
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(sectionFocusRequester)
|
||||
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -253,11 +198,8 @@ fun TvNextUpSection(
|
||||
) { index, item ->
|
||||
TvNextUpCard(
|
||||
item = item,
|
||||
focusRequester = if (index == 0) firstItemFocusRequester else null,
|
||||
isFirstItem = index == 0,
|
||||
isLastItem = index == items.lastIndex,
|
||||
upFocusRequester = upFocusRequester,
|
||||
downFocusRequester = downFocusRequester,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
)
|
||||
}
|
||||
@@ -268,61 +210,24 @@ fun TvNextUpSection(
|
||||
fun TvNextUpCard(
|
||||
item: NextUpItem,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester? = null,
|
||||
isFirstItem: Boolean = false,
|
||||
isLastItem: Boolean = false,
|
||||
upFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
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 cardHeight = cardWidth * 9 / 16
|
||||
|
||||
fun openItem(item: NextUpItem) {
|
||||
val episode = item.episode
|
||||
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(
|
||||
modifier = modifier
|
||||
.width(cardWidth)
|
||||
@@ -346,9 +251,9 @@ fun TvNextUpCard(
|
||||
.background(scheme.surfaceVariant)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = imageRequest,
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = imageFocusModifier
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable {
|
||||
@@ -382,10 +287,6 @@ fun TvLibraryPosterSection(
|
||||
title: String,
|
||||
items: List<PosterItem>,
|
||||
action: String?,
|
||||
sectionFocusRequester: FocusRequester,
|
||||
firstItemFocusRequester: FocusRequester? = null,
|
||||
upFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onSeriesSelected: (UUID) -> Unit,
|
||||
@@ -397,9 +298,7 @@ fun TvLibraryPosterSection(
|
||||
)
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(sectionFocusRequester)
|
||||
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -409,9 +308,6 @@ fun TvLibraryPosterSection(
|
||||
) { index, item ->
|
||||
PosterCard(
|
||||
item = item,
|
||||
focusRequester = if (index == 0) firstItemFocusRequester else null,
|
||||
upFocusRequester = upFocusRequester,
|
||||
downFocusRequester = downFocusRequester,
|
||||
onMovieSelected = onMovieSelected,
|
||||
onSeriesSelected = onSeriesSelected,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
package hu.bbara.purefin.tv.home.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -17,7 +12,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -30,25 +24,10 @@ fun TvHomeTopBar(
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val safeSelectedTabIndex = selectedTabIndex.coerceIn(0, tabs.lastIndex.coerceAtLeast(0))
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(scheme.background.copy(alpha = 0.95f))
|
||||
.zIndex(1f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = safeSelectedTabIndex,
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = scheme.surface,
|
||||
contentColor = scheme.onSurface
|
||||
contentColor = scheme.onSurface,
|
||||
) {
|
||||
tabs.forEachIndexed { index, tab ->
|
||||
Tab(
|
||||
@@ -70,5 +49,3 @@ fun TvHomeTopBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Route
|
||||
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.player.TvPlayerScreen
|
||||
|
||||
fun EntryProviderScope<Route>.tvHomeSection() {
|
||||
entry<Route.Home> {
|
||||
TvHomeScreen()
|
||||
TvAppScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,218 +5,24 @@ import androidx.compose.ui.graphics.Color
|
||||
private fun hslColor(hue: Float, saturation: Float, lightness: Float): Color =
|
||||
Color.hsl(hue = hue, saturation = saturation / 100f, lightness = lightness / 100f)
|
||||
|
||||
val primaryLight = hslColor(26.9f, 61.4f, 33.5f)
|
||||
val onPrimaryLight = hslColor(0f, 0f, 100f)
|
||||
val primaryContainerLight = hslColor(23.8f, 100f, 88.6f)
|
||||
val onPrimaryContainerLight = hslColor(28.2f, 81.7f, 23.5f)
|
||||
val secondaryLight = hslColor(25f, 25.8f, 36.5f)
|
||||
val onSecondaryLight = hslColor(0f, 0f, 100f)
|
||||
val secondaryContainerLight = hslColor(23.8f, 100f, 88.6f)
|
||||
val onSecondaryContainerLight = hslColor(24.5f, 31.9f, 27.1f)
|
||||
val tertiaryLight = hslColor(64.1f, 29.3f, 29.4f)
|
||||
val onTertiaryLight = hslColor(0f, 0f, 100f)
|
||||
val tertiaryContainerLight = hslColor(62.1f, 52.8f, 79.2f)
|
||||
val onTertiaryContainerLight = hslColor(64.4f, 39.0f, 20.6f)
|
||||
val errorLight = hslColor(0f, 75.5f, 41.6f)
|
||||
val onErrorLight = hslColor(0f, 0f, 100f)
|
||||
val errorContainerLight = hslColor(5.9f, 100f, 92.0f)
|
||||
val onErrorContainerLight = hslColor(355.9f, 100f, 28.8f)
|
||||
val backgroundLight = hslColor(18f, 100f, 98.0f)
|
||||
val onBackgroundLight = hslColor(23.1f, 23.6f, 10.8f)
|
||||
val surfaceLight = hslColor(18f, 100f, 98.0f)
|
||||
val onSurfaceLight = hslColor(23.1f, 23.6f, 10.8f)
|
||||
val surfaceVariantLight = hslColor(23.6f, 57.9f, 88.8f)
|
||||
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)
|
||||
val primaryDark = hslColor(255f, 70f, 70f)
|
||||
val onPrimaryDark = hslColor(255f, 60f, 12f)
|
||||
val primaryContainerDark = hslColor(255f, 70f, 70f)
|
||||
val onPrimaryContainerDark = hslColor(255f, 60f, 12f)
|
||||
val secondaryDark = hslColor(255f, 40f, 40f)
|
||||
val onSecondaryDark = hslColor(255f, 40f, 88f)
|
||||
val secondaryContainerDark = hslColor(255f, 40f, 40f)
|
||||
val onSecondaryContainerDark = hslColor(255f, 40f, 88f)
|
||||
val tertiaryDark = hslColor(255f, 15f, 65f)
|
||||
val onTertiaryDark = hslColor(255f, 15f, 12f)
|
||||
val tertiaryContainerDark = hslColor(255f, 15f, 65.0f)
|
||||
val onTertiaryContainerDark = hslColor(255f, 15f, 12f)
|
||||
val errorDark = hslColor(7f, 100f, 84f)
|
||||
val onErrorDark = hslColor(357f, 100f, 21f)
|
||||
val errorContainerDark = hslColor(355f, 100f, 29f)
|
||||
val onErrorContainerDark = hslColor(6f, 100f, 92f)
|
||||
val backgroundDark = hslColor(255f, 60f, 8f)
|
||||
val onBackgroundDark = hslColor(255f, 60f, 94f)
|
||||
val surfaceDark = hslColor(255f, 40f, 12f)
|
||||
val onSurfaceDark = hslColor(255f, 40f, 92f)
|
||||
val surfaceContainerDark = hslColor(255f, 40f, 12f)
|
||||
@@ -5,50 +5,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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(
|
||||
primary = primaryDark,
|
||||
onPrimary = onPrimaryDark,
|
||||
@@ -70,173 +30,7 @@ private val darkScheme = darkColorScheme(
|
||||
onBackground = onBackgroundDark,
|
||||
surface = surfaceDark,
|
||||
onSurface = onSurfaceDark,
|
||||
surfaceVariant = surfaceVariantDark,
|
||||
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,
|
||||
surfaceContainer = surfaceContainerDark
|
||||
)
|
||||
|
||||
@Immutable
|
||||
@@ -247,10 +41,6 @@ data class ColorFamily(
|
||||
val onColorContainer: Color
|
||||
)
|
||||
|
||||
val unspecified_scheme = ColorFamily(
|
||||
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
@@ -264,7 +54,7 @@ fun AppTheme(
|
||||
}
|
||||
|
||||
darkTheme -> darkScheme
|
||||
else -> lightScheme
|
||||
else -> darkScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
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.LocalNavigationBackStack
|
||||
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.shared.content.episode.EpisodeScreenViewModel
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -162,7 +164,7 @@ private fun EpisodeHeroSection(
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = episode.heroImageUrl,
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
backgroundColor = scheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
@@ -258,7 +260,7 @@ private fun previewEpisode(): Episode {
|
||||
progress = 63.0,
|
||||
watched = false,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee",
|
||||
cast = listOf(
|
||||
CastMember("Adam Scott", "Mark Scout", null),
|
||||
CastMember("Britt Lower", "Helly R.", null),
|
||||
|
||||
@@ -35,12 +35,14 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
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.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.download.DownloadState
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -135,7 +137,7 @@ fun MediaHeroSection(
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = movie.heroImageUrl,
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
@@ -209,7 +211,7 @@ private fun previewMovie(): Movie =
|
||||
runtime = "2h 44m",
|
||||
format = "Dolby Vision",
|
||||
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",
|
||||
subtitles = "English CC",
|
||||
cast = listOf(
|
||||
|
||||
@@ -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.PurefinAsyncImage
|
||||
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.LocalNavigationManager
|
||||
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.shared.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.player.PlayerActivity
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
internal fun SeriesTopBar(
|
||||
@@ -347,7 +349,7 @@ private fun EpisodeCard(
|
||||
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = episode.heroImageUrl,
|
||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
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.model.CastMember
|
||||
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.shared.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -198,7 +200,7 @@ private fun SeriesHeroSection(
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = series.heroImageUrl,
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
|
||||
backgroundColor = scheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
@@ -270,7 +272,7 @@ private fun previewSeries(): Series {
|
||||
progress = 100.0,
|
||||
watched = true,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1497032205916-ac775f0649ae",
|
||||
cast = emptyList()
|
||||
),
|
||||
Episode(
|
||||
@@ -286,7 +288,7 @@ private fun previewSeries(): Series {
|
||||
progress = 34.0,
|
||||
watched = false,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1520034475321-cbe63696469a",
|
||||
cast = emptyList()
|
||||
)
|
||||
)
|
||||
@@ -304,7 +306,7 @@ private fun previewSeries(): Series {
|
||||
progress = null,
|
||||
watched = false,
|
||||
format = "4K",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429",
|
||||
cast = emptyList()
|
||||
)
|
||||
)
|
||||
@@ -315,7 +317,7 @@ private fun previewSeries(): Series {
|
||||
name = "Constellation",
|
||||
synopsis = "When an experiment in orbit goes wrong, the survivors return home to a world that no longer fits their memories.",
|
||||
year = "2024",
|
||||
heroImageUrl = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa",
|
||||
imageUrlPrefix = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa",
|
||||
unwatchedEpisodeCount = 2,
|
||||
seasonCount = 2,
|
||||
seasons = listOf(
|
||||
|
||||
@@ -21,26 +21,19 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.library.LibraryPosterSection
|
||||
import hu.bbara.purefin.app.home.ui.nextup.NextUpSection
|
||||
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.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
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.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import java.util.UUID as JavaUuid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -170,265 +163,3 @@ 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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||
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 org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
internal fun ContinueWatchingCard(
|
||||
@@ -51,8 +53,14 @@ internal fun ContinueWatchingCard(
|
||||
else -> ""
|
||||
}
|
||||
val imageUrl = when (item.type) {
|
||||
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
|
||||
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
|
||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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 org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
@Composable
|
||||
internal fun NextUpCard(
|
||||
@@ -55,7 +57,7 @@ internal fun NextUpCard(
|
||||
.background(scheme.surface)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = item.episode.heroImageUrl,
|
||||
model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
contentDescription = item.primaryText,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -433,10 +433,9 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
runtime = formatRuntime(this.runTimeTicks),
|
||||
synopsis = this.overview ?: "No synopsis available",
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = this.id,
|
||||
type = ImageType.PRIMARY
|
||||
),
|
||||
subtitles = "ENG",
|
||||
audioTrack = "ENG",
|
||||
@@ -452,10 +451,9 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
synopsis = this.overview ?: "No synopsis available",
|
||||
year = this.productionYear?.toString()
|
||||
?: this.premiereDate?.year?.toString().orEmpty(),
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = this.id,
|
||||
type = ImageType.PRIMARY
|
||||
itemId = this.id
|
||||
),
|
||||
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
|
||||
seasonCount = this.childCount!!,
|
||||
@@ -478,11 +476,10 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
|
||||
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
val releaseDate = formatReleaseDate(premiereDate, productionYear)
|
||||
val heroImageUrl = id?.let { itemId ->
|
||||
JellyfinImageHelper.toImageUrl(
|
||||
val imageUrlPrefix = id?.let { itemId ->
|
||||
JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = itemId,
|
||||
type = ImageType.PRIMARY
|
||||
itemId = itemId
|
||||
)
|
||||
} ?: ""
|
||||
return Episode(
|
||||
@@ -498,7 +495,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
watched = userData!!.played,
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
synopsis = overview ?: "No synopsis available.",
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
|
||||
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
val releaseDate = formatReleaseDate(premiereDate, productionYear)
|
||||
val heroImageUrl = id?.let { itemId ->
|
||||
JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = itemId, type = ImageType.PRIMARY)
|
||||
val imageUrlPrefix = id?.let { itemId ->
|
||||
JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = itemId)
|
||||
} ?: ""
|
||||
return Episode(
|
||||
id = id,
|
||||
@@ -141,7 +141,7 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
watched = userData!!.played,
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
synopsis = overview ?: "No synopsis available.",
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
class JellyfinImageHelper {
|
||||
companion object {
|
||||
|
||||
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
@@ -17,5 +18,28 @@ class JellyfinImageHelper {
|
||||
.append(type.serialName)
|
||||
.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()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,5 +31,5 @@ data class EpisodeEntity(
|
||||
val progress: Double?,
|
||||
val watched: Boolean,
|
||||
val format: String,
|
||||
val heroImageUrl: String
|
||||
val imageUrlPrefix: String
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package hu.bbara.purefin.core.data.room.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
@@ -21,7 +20,7 @@ data class MovieEntity(
|
||||
val runtime: String,
|
||||
val format: String,
|
||||
val synopsis: String,
|
||||
val heroImageUrl: String,
|
||||
val imageUrlPrefix: String,
|
||||
val audioTrack: String,
|
||||
val subtitles: String
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package hu.bbara.purefin.core.data.room.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
@@ -16,7 +15,7 @@ data class SeriesEntity(
|
||||
val name: String,
|
||||
val synopsis: String,
|
||||
val year: String,
|
||||
val heroImageUrl: String,
|
||||
val imageUrlPrefix: String,
|
||||
val unwatchedEpisodeCount: Int,
|
||||
val seasonCount: Int
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity
|
||||
EpisodeEntity::class,
|
||||
SmartDownloadEntity::class,
|
||||
],
|
||||
version = 6,
|
||||
version = 9,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(UuidConverters::class)
|
||||
|
||||
@@ -208,7 +208,7 @@ class OfflineRoomMediaLocalDataSource(
|
||||
runtime = runtime,
|
||||
format = format,
|
||||
synopsis = synopsis,
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
audioTrack = audioTrack,
|
||||
subtitles = subtitles
|
||||
)
|
||||
@@ -219,7 +219,7 @@ class OfflineRoomMediaLocalDataSource(
|
||||
name = name,
|
||||
synopsis = synopsis,
|
||||
year = year,
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
unwatchedEpisodeCount = unwatchedEpisodeCount,
|
||||
seasonCount = seasonCount
|
||||
)
|
||||
@@ -246,7 +246,7 @@ class OfflineRoomMediaLocalDataSource(
|
||||
progress = progress,
|
||||
watched = watched,
|
||||
format = format,
|
||||
heroImageUrl = heroImageUrl
|
||||
imageUrlPrefix = imageUrlPrefix
|
||||
)
|
||||
|
||||
private fun MovieEntity.toDomain() = Movie(
|
||||
@@ -260,7 +260,7 @@ class OfflineRoomMediaLocalDataSource(
|
||||
runtime = runtime,
|
||||
format = format,
|
||||
synopsis = synopsis,
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
audioTrack = audioTrack,
|
||||
subtitles = subtitles,
|
||||
cast = emptyList()
|
||||
@@ -272,7 +272,7 @@ class OfflineRoomMediaLocalDataSource(
|
||||
name = name,
|
||||
synopsis = synopsis,
|
||||
year = year,
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
unwatchedEpisodeCount = unwatchedEpisodeCount,
|
||||
seasonCount = seasonCount,
|
||||
seasons = seasons,
|
||||
@@ -302,7 +302,7 @@ class OfflineRoomMediaLocalDataSource(
|
||||
progress = progress,
|
||||
watched = watched,
|
||||
format = format,
|
||||
heroImageUrl = heroImageUrl,
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,6 @@ data class Episode(
|
||||
val progress: Double?,
|
||||
val watched: Boolean,
|
||||
val format: String,
|
||||
val heroImageUrl: String,
|
||||
val imageUrlPrefix: String,
|
||||
val cast: List<CastMember>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ data class Movie(
|
||||
val runtime: String,
|
||||
val format: String,
|
||||
val synopsis: String,
|
||||
val heroImageUrl: String,
|
||||
val imageUrlPrefix: String,
|
||||
val audioTrack: String,
|
||||
val subtitles: String,
|
||||
val cast: List<CastMember>
|
||||
|
||||
@@ -8,7 +8,7 @@ data class Series(
|
||||
val name: String,
|
||||
val synopsis: String,
|
||||
val year: String,
|
||||
val heroImageUrl: String,
|
||||
val imageUrlPrefix: String,
|
||||
val unwatchedEpisodeCount: Int,
|
||||
val seasonCount: Int,
|
||||
val seasons: List<Season>,
|
||||
|
||||
@@ -124,6 +124,7 @@ class PlayerMediaRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun buildOfflineMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? {
|
||||
val download = downloadManager.downloadIndex.getDownload(mediaId.toString())
|
||||
?.takeIf { it.state == Download.STATE_COMPLETED }
|
||||
@@ -138,8 +139,8 @@ class PlayerMediaRepository @Inject constructor(
|
||||
}
|
||||
val subtitle = episode?.let { episodeSubtitle(null, it.index) }
|
||||
val artworkUrl = when {
|
||||
movie != null -> movie.heroImageUrl
|
||||
episode != null -> episode.heroImageUrl
|
||||
movie != null -> JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY)
|
||||
episode != null -> JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY)
|
||||
else -> JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY)
|
||||
}
|
||||
val resumePositionMs = resumePositionFor(movie, episode)
|
||||
|
||||
@@ -23,7 +23,6 @@ import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -32,6 +31,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
@@ -156,10 +156,9 @@ class MediaDownloadManager @Inject constructor(
|
||||
runtime = formatRuntime(itemInfo.runTimeTicks),
|
||||
synopsis = itemInfo.overview ?: "No synopsis available",
|
||||
format = itemInfo.container?.uppercase() ?: "VIDEO",
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = itemInfo.id,
|
||||
type = ImageType.PRIMARY
|
||||
itemId = itemInfo.id
|
||||
),
|
||||
subtitles = "ENG",
|
||||
audioTrack = "ENG",
|
||||
@@ -370,10 +369,9 @@ class MediaDownloadManager @Inject constructor(
|
||||
watched = userData?.played ?: false,
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
synopsis = overview ?: "No synopsis available.",
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = id,
|
||||
type = ImageType.PRIMARY
|
||||
itemId = id
|
||||
),
|
||||
cast = emptyList()
|
||||
)
|
||||
@@ -386,10 +384,9 @@ class MediaDownloadManager @Inject constructor(
|
||||
name = name ?: "Unknown",
|
||||
synopsis = overview ?: "No synopsis available",
|
||||
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = id,
|
||||
type = ImageType.PRIMARY
|
||||
itemId = id
|
||||
),
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
seasonCount = childCount ?: 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
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.NavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -70,7 +72,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
contentId = contentId,
|
||||
title = movie.title,
|
||||
subtitle = "",
|
||||
imageUrl = movie.heroImageUrl,
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
||||
progress = progress
|
||||
)
|
||||
} else {
|
||||
@@ -80,7 +82,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
contentId = contentId,
|
||||
title = it.title,
|
||||
subtitle = seriesMap[it.seriesId]?.name ?: "",
|
||||
imageUrl = it.heroImageUrl,
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(it.imageUrlPrefix, ImageType.PRIMARY),
|
||||
progress = progress
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
|
||||
sealed interface SuggestedItem {
|
||||
@@ -36,7 +38,10 @@ data class SuggestedEpisode (
|
||||
override val id: UUID = episode.id,
|
||||
override val title: String = episode.title,
|
||||
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
|
||||
|
||||
data class SuggestedSeries (
|
||||
@@ -56,7 +61,10 @@ data class SuggestedSeries (
|
||||
override val id: UUID = series.id,
|
||||
override val title: String = series.name,
|
||||
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
|
||||
|
||||
data class SuggestedMovie (
|
||||
@@ -74,7 +82,10 @@ data class SuggestedMovie (
|
||||
override val id: UUID = movie.id,
|
||||
override val title: String = movie.title,
|
||||
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
|
||||
|
||||
data class ContinueWatchingItem(
|
||||
@@ -139,9 +150,18 @@ data class PosterItem(
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
val imageUrl: String = when (type) {
|
||||
BaseItemKind.MOVIE -> movie!!.heroImageUrl
|
||||
BaseItemKind.EPISODE -> episode!!.heroImageUrl
|
||||
BaseItemKind.SERIES -> series!!.heroImageUrl
|
||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
prefixImageUrl = movie!!.imageUrlPrefix,
|
||||
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")
|
||||
}
|
||||
fun watched() = when (type) {
|
||||
|
||||
@@ -16,6 +16,7 @@ datastore = "1.1.1"
|
||||
kotlinxSerializationJson = "1.7.3"
|
||||
okhttp = "4.12.0"
|
||||
foundation = "1.10.1"
|
||||
tvMaterial = "1.0.1"
|
||||
coil = "3.3.0"
|
||||
media3 = "1.9.0"
|
||||
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-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
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" }
|
||||
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
|
||||
Reference in New Issue
Block a user