mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Compare commits
9 Commits
6341510cc2
...
71a9a8b98e
| Author | SHA1 | Date | |
|---|---|---|---|
| 71a9a8b98e | |||
| f304b1f1f5 | |||
| e738e9637f | |||
| bfc007f24f | |||
| d815812e79 | |||
| b0baad7f8c | |||
| 3b95185f98 | |||
| 72fea00444 | |||
| 8f5d0c5bc1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@
|
|||||||
local.properties
|
local.properties
|
||||||
.codex/
|
.codex/
|
||||||
openspec
|
openspec
|
||||||
|
*.patch
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.hilt)
|
alias(libs.plugins.hilt)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
|
alias(libs.plugins.google.gms.google.services)
|
||||||
|
alias(libs.plugins.google.firebase.crashlytics)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -82,6 +84,7 @@ dependencies {
|
|||||||
implementation(libs.media3.datasource.okhttp)
|
implementation(libs.media3.datasource.okhttp)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
|
implementation(libs.firebase.crashlytics)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
29
app-tv/google-services.json
Normal file
29
app-tv/google-services.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "407366050869",
|
||||||
|
"project_id": "purefin-12eec",
|
||||||
|
"storage_bucket": "purefin-12eec.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:407366050869:android:8fa3540adfc514add73d7c",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "hu.bbara.purefin.tv"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyD8a9BnlFSmcw4v0gukC1qcyESznUUWsXI"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -105,7 +105,9 @@ class EpisodeScreenContentTest {
|
|||||||
return Episode(
|
return Episode(
|
||||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Severance",
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = 2,
|
||||||
index = 4,
|
index = 4,
|
||||||
title = "The You You Are",
|
title = "The You You Are",
|
||||||
synopsis = "Mark is pulled deeper into Lumon's fractured world as the team chases a clue.",
|
synopsis = "Mark is pulled deeper into Lumon's fractured world as the team chases a clue.",
|
||||||
|
|||||||
@@ -425,7 +425,9 @@ class TvHomeContentTest {
|
|||||||
return Episode(
|
return Episode(
|
||||||
id = UUID.fromString(id),
|
id = UUID.fromString(id),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Firefly",
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = 1,
|
||||||
index = 1,
|
index = 1,
|
||||||
title = title,
|
title = title,
|
||||||
synopsis = "A crew member takes the shuttle for a spin and makes a mess.",
|
synopsis = "A crew member takes the shuttle for a spin and makes a mess.",
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ class SeriesScreenContentTest {
|
|||||||
Episode(
|
Episode(
|
||||||
id = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
id = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Severance",
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = 1,
|
||||||
index = 1,
|
index = 1,
|
||||||
title = "Good News About Hell",
|
title = "Good News About Hell",
|
||||||
synopsis = "Mark is promoted after an unexpected tragedy.",
|
synopsis = "Mark is promoted after an unexpected tragedy.",
|
||||||
@@ -136,7 +138,9 @@ class SeriesScreenContentTest {
|
|||||||
Episode(
|
Episode(
|
||||||
id = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
id = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Severance",
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = 1,
|
||||||
index = 2,
|
index = 2,
|
||||||
title = "Half Loop",
|
title = "Half Loop",
|
||||||
synopsis = "Mark takes the team out for a sanctioned dinner.",
|
synopsis = "Mark takes the team out for a sanctioned dinner.",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package hu.bbara.purefin.navigation
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||||
import hu.bbara.purefin.ui.screen.episode.TvEpisodeScreen
|
|
||||||
import hu.bbara.purefin.ui.screen.movie.TvMovieScreen
|
import hu.bbara.purefin.ui.screen.movie.TvMovieScreen
|
||||||
import hu.bbara.purefin.ui.screen.series.TvSeriesScreen
|
import hu.bbara.purefin.ui.screen.series.TvSeriesScreen
|
||||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
@@ -37,7 +36,10 @@ fun EntryProviderScope<Route>.tvSeriesSection() {
|
|||||||
|
|
||||||
fun EntryProviderScope<Route>.tvEpisodeSection() {
|
fun EntryProviderScope<Route>.tvEpisodeSection() {
|
||||||
entry<Route.EpisodeRoute> { route ->
|
entry<Route.EpisodeRoute> { route ->
|
||||||
TvEpisodeScreen(episode = route.item)
|
TvSeriesScreen(
|
||||||
|
series = SeriesDto(id = route.item.seriesId),
|
||||||
|
focusedEpisodeId = route.item.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||||
import hu.bbara.purefin.ui.model.SeriesUiModel
|
import hu.bbara.purefin.ui.model.SeriesUiModel
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,7 +43,6 @@ fun PosterCard(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageModifier: Modifier = Modifier,
|
imageModifier: Modifier = Modifier,
|
||||||
posterWidth: Dp = 144.dp,
|
posterWidth: Dp = 144.dp,
|
||||||
contentScale: Float = 1f,
|
|
||||||
showSecondaryText: Boolean = false,
|
showSecondaryText: Boolean = false,
|
||||||
indicatorSize: Int = 28,
|
indicatorSize: Int = 28,
|
||||||
indicatorPadding: Dp = 8.dp,
|
indicatorPadding: Dp = 8.dp,
|
||||||
@@ -65,7 +64,6 @@ fun PosterCard(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
imageModifier = imageModifier,
|
imageModifier = imageModifier,
|
||||||
posterWidth = posterWidth,
|
posterWidth = posterWidth,
|
||||||
contentScale = contentScale,
|
|
||||||
showSecondaryText = showSecondaryText,
|
showSecondaryText = showSecondaryText,
|
||||||
indicatorSize = indicatorSize,
|
indicatorSize = indicatorSize,
|
||||||
indicatorPadding = indicatorPadding,
|
indicatorPadding = indicatorPadding,
|
||||||
@@ -82,11 +80,10 @@ fun PosterCardContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageModifier: Modifier = Modifier,
|
imageModifier: Modifier = Modifier,
|
||||||
posterWidth: Dp = 144.dp,
|
posterWidth: Dp = 144.dp,
|
||||||
contentScale: Float = 1f,
|
|
||||||
showSecondaryText: Boolean = false,
|
showSecondaryText: Boolean = false,
|
||||||
indicatorSize: Int = 28,
|
indicatorSize: Int = 28,
|
||||||
indicatorPadding: Dp = 8.dp,
|
indicatorPadding: Dp = 8.dp,
|
||||||
onFocused: () -> Unit = {},
|
onFocused: () -> Unit,
|
||||||
focusedScale: Float = 1f,
|
focusedScale: Float = 1f,
|
||||||
focusedBorderWidth: Dp = 1.dp,
|
focusedBorderWidth: Dp = 1.dp,
|
||||||
focusedTransformOrigin: TransformOrigin = TransformOrigin(0.5f, 0f)
|
focusedTransformOrigin: TransformOrigin = TransformOrigin(0.5f, 0f)
|
||||||
@@ -155,6 +152,7 @@ fun PosterCardContent(
|
|||||||
text = model.primaryText,
|
text = model.primaryText,
|
||||||
color = scheme.onBackground,
|
color = scheme.onBackground,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
|
lineHeight = 13.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
@@ -167,6 +165,7 @@ fun PosterCardContent(
|
|||||||
text = text,
|
text = text,
|
||||||
color = scheme.onSurfaceVariant,
|
color = scheme.onSurfaceVariant,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.compose.foundation.gestures.BringIntoViewSpec
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
internal val TvHomeFocusedItemTopOffset = 42.dp
|
internal val TvHomeFocusedItemTopOffset = 48.dp
|
||||||
internal val TvHomeBringIntoViewTrailingSpace = 120.dp
|
internal val TvHomeBringIntoViewTrailingSpace = 120.dp
|
||||||
|
|
||||||
private const val TvHomeRowPivotParentFraction = 0.3f
|
private const val TvHomeRowPivotParentFraction = 0.3f
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package hu.bbara.purefin.ui.screen.home.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -20,11 +21,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
|
||||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||||
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||||
@@ -46,9 +47,6 @@ fun TvHomeContent(
|
|||||||
val hasContinueWatching = continueWatching.isNotEmpty()
|
val hasContinueWatching = continueWatching.isNotEmpty()
|
||||||
val hasNextUp = nextUp.isNotEmpty()
|
val hasNextUp = nextUp.isNotEmpty()
|
||||||
val hasLibraryItems = libraries.any { libraryContent[it.id].orEmpty().isNotEmpty() }
|
val hasLibraryItems = libraries.any { libraryContent[it.id].orEmpty().isNotEmpty() }
|
||||||
val firstLibraryWithItemsId = libraries.firstOrNull {
|
|
||||||
libraryContent[it.id].orEmpty().isNotEmpty()
|
|
||||||
}?.id
|
|
||||||
val hasVisibleContent = hasContinueWatching || hasNextUp
|
val hasVisibleContent = hasContinueWatching || hasNextUp
|
||||||
val hasInitialFocusableItem = hasContinueWatching || hasNextUp || hasLibraryItems
|
val hasInitialFocusableItem = hasContinueWatching || hasNextUp || hasLibraryItems
|
||||||
val initialFocusRequester = remember { FocusRequester() }
|
val initialFocusRequester = remember { FocusRequester() }
|
||||||
@@ -68,21 +66,16 @@ fun TvHomeContent(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(scheme.background)
|
.background(scheme.background)
|
||||||
.testTag(TvHomeContentViewportTag),
|
.focusRequester(initialFocusRequester),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
contentPadding = contentPadding
|
contentPadding = contentPadding
|
||||||
) {
|
) {
|
||||||
item(key = "tv-home-top-spacer") {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
if (hasContinueWatching) {
|
if (hasContinueWatching) {
|
||||||
item(key = "tv-home-continue-watching") {
|
item(key = "tv-home-continue-watching") {
|
||||||
TvContinueWatchingSection(
|
TvContinueWatchingSection(
|
||||||
items = continueWatching,
|
items = continueWatching,
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
onMediaSelected = onMediaSelected,
|
onMediaSelected = onMediaSelected,
|
||||||
firstItemFocusRequester = initialFocusRequester,
|
|
||||||
firstItemTestTag = TvHomeInitialFocusTag,
|
|
||||||
rowTestTag = TvHomeContinueWatchingRowTag
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +86,6 @@ fun TvHomeContent(
|
|||||||
items = nextUp,
|
items = nextUp,
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
onMediaSelected = onMediaSelected,
|
onMediaSelected = onMediaSelected,
|
||||||
firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching },
|
|
||||||
firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching },
|
|
||||||
rowTestTag = TvHomeNextUpRowTag
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,10 +98,6 @@ fun TvHomeContent(
|
|||||||
title = library.name,
|
title = library.name,
|
||||||
items = libraryContent[library.id].orEmpty(),
|
items = libraryContent[library.id].orEmpty(),
|
||||||
onFocusedItem = onMediaFocused,
|
onFocusedItem = onMediaFocused,
|
||||||
firstItemFocusRequester = initialFocusRequester.takeIf {
|
|
||||||
!hasContinueWatching && !hasNextUp && library.id == firstLibraryWithItemsId
|
|
||||||
},
|
|
||||||
firstItemTestTag = tvHomeLibraryFirstItemTag(library.id),
|
|
||||||
onMediaSelected = onMediaSelected
|
onMediaSelected = onMediaSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,24 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -28,26 +31,27 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.card.PosterCardContent
|
import hu.bbara.purefin.ui.common.card.PosterCardContent
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -61,15 +65,14 @@ private val TvHomeLandscapeFocusedBorderWidth = (2f * TvHomeMediaCardScale).dp
|
|||||||
private val TvHomeLandscapeUnfocusedBorderWidth = (1f * TvHomeMediaCardScale).dp
|
private val TvHomeLandscapeUnfocusedBorderWidth = (1f * TvHomeMediaCardScale).dp
|
||||||
private val TvHomeLandscapeProgressBarHeight = (6f * TvHomeMediaCardScale).dp
|
private val TvHomeLandscapeProgressBarHeight = (6f * TvHomeMediaCardScale).dp
|
||||||
private val TvHomeLandscapeProgressBarPadding = (8f * TvHomeMediaCardScale).dp
|
private val TvHomeLandscapeProgressBarPadding = (8f * TvHomeMediaCardScale).dp
|
||||||
private val TvHomeLandscapeMetadataSpacing = (4f * TvHomeMediaCardScale).dp
|
private val TvHomeLandscapeMetadataSpacing = (1f * TvHomeMediaCardScale).dp
|
||||||
private val TvHomeLandscapeMetadataTopPadding = (12f * TvHomeMediaCardScale).dp
|
private val TvHomeLandscapeMetadataTopPadding = (6f * TvHomeMediaCardScale).dp
|
||||||
private val TvHomeLandscapeMetadataHorizontalPadding = (4f * TvHomeMediaCardScale).dp
|
private val TvHomeLandscapeMetadataHorizontalPadding = (4f * TvHomeMediaCardScale).dp
|
||||||
private val TvHomeLandscapeTitleFontSize = (15f * TvHomeMediaCardScale).sp
|
private val TvHomeLandscapeTitleFontSize = (15f * TvHomeMediaCardScale).sp
|
||||||
private val TvHomeLandscapeSupportingFontSize = (11f * TvHomeMediaCardScale).sp
|
private val TvHomeLandscapeSupportingFontSize = (11f * TvHomeMediaCardScale).sp
|
||||||
private val TvHomePosterIndicatorSize = (24f * TvHomeMediaCardScale).roundToInt()
|
private val TvHomePosterIndicatorSize = (24f * TvHomeMediaCardScale).roundToInt()
|
||||||
private val TvHomePosterIndicatorPadding = (6f * TvHomeMediaCardScale).dp
|
private val TvHomePosterIndicatorPadding = (6f * TvHomeMediaCardScale).dp
|
||||||
internal const val TvHomeSectionRowTagPrefix = "tv-home-section-row-"
|
internal const val TvHomeSectionRowTagPrefix = "tv-home-section-row-"
|
||||||
internal const val TvHomeContinueWatchingRowTag = "${TvHomeSectionRowTagPrefix}continue-watching"
|
|
||||||
internal const val TvHomeNextUpRowTag = "${TvHomeSectionRowTagPrefix}next-up"
|
internal const val TvHomeNextUpRowTag = "${TvHomeSectionRowTagPrefix}next-up"
|
||||||
|
|
||||||
internal fun tvHomeLibraryRowTag(libraryId: UUID): String =
|
internal fun tvHomeLibraryRowTag(libraryId: UUID): String =
|
||||||
@@ -81,91 +84,59 @@ internal fun tvHomeLibraryFirstItemTag(libraryId: UUID): String =
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvContinueWatchingSection(
|
fun TvContinueWatchingSection(
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
|
||||||
firstItemTestTag: String? = null,
|
|
||||||
rowTestTag: String? = null,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
TvSectionHeader(
|
TvSectionHeader(
|
||||||
title = "Continue Watching",
|
title = "Continue Watching",
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TvHomeSectionRow(
|
TvHomeSectionRow(
|
||||||
|
items = items,
|
||||||
|
onFocused = onFocusedItem,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
rowTestTag = rowTestTag
|
) { item, onFocused, focusModifier ->
|
||||||
) {
|
TvHomeLandscapeCard(
|
||||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
title = item.primaryText,
|
||||||
TvHomeLandscapeCard(
|
supporting = item.secondaryText,
|
||||||
title = item.primaryText,
|
imageUrl = item.primaryImageUrl,
|
||||||
supporting = item.secondaryText,
|
progress = item.progress ?: 0f,
|
||||||
imageUrl = item.primaryImageUrl,
|
imageModifier = focusModifier,
|
||||||
progress = item.progress ?: 0f,
|
onFocusedItem = {
|
||||||
imageModifier = Modifier
|
onFocused(item)
|
||||||
.then(
|
},
|
||||||
if (index == 0 && firstItemFocusRequester != null) {
|
onClick = { onMediaSelected(item) }
|
||||||
Modifier.focusRequester(firstItemFocusRequester)
|
)
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (index == 0 && firstItemTestTag != null) {
|
|
||||||
Modifier.testTag(firstItemTestTag)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
),
|
|
||||||
onFocusedItem = { onFocusedItem(item) },
|
|
||||||
onClick = { onMediaSelected(item) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvNextUpSection(
|
fun TvNextUpSection(
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
|
||||||
firstItemTestTag: String? = null,
|
|
||||||
rowTestTag: String? = null,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
TvSectionHeader(
|
TvSectionHeader(
|
||||||
title = "Next Up",
|
title = "Next Up",
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TvHomeSectionRow(
|
TvHomeSectionRow(
|
||||||
|
items = items,
|
||||||
|
onFocused = onFocusedItem,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
rowTestTag = rowTestTag
|
) { item, onFocused, focusModifier ->
|
||||||
) {
|
TvHomeLandscapeCard(
|
||||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
title = item.primaryText,
|
||||||
TvHomeLandscapeCard(
|
supporting = item.secondaryText,
|
||||||
title = item.primaryText,
|
imageUrl = item.primaryImageUrl,
|
||||||
supporting = item.secondaryText,
|
imageModifier = focusModifier,
|
||||||
imageUrl = item.primaryImageUrl,
|
onFocusedItem = { onFocused(item) },
|
||||||
imageModifier = Modifier
|
onClick = { onMediaSelected(item) }
|
||||||
.then(
|
)
|
||||||
if (index == 0 && firstItemFocusRequester != null) {
|
|
||||||
Modifier.focusRequester(firstItemFocusRequester)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (index == 0 && firstItemTestTag != null) {
|
|
||||||
Modifier.testTag(firstItemTestTag)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
),
|
|
||||||
onFocusedItem = { onFocusedItem(item) },
|
|
||||||
onClick = { onMediaSelected(item) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,74 +144,75 @@ fun TvNextUpSection(
|
|||||||
fun TvLibraryPosterSection(
|
fun TvLibraryPosterSection(
|
||||||
title: String,
|
title: String,
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
onFocusedItem: (MediaUiModel) -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
|
||||||
firstItemTestTag: String? = null,
|
|
||||||
rowTestTag: String? = null,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
TvSectionHeader(
|
TvSectionHeader(
|
||||||
title = title,
|
title = title,
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TvHomeSectionRow(
|
TvHomeSectionRow(
|
||||||
|
items = items,
|
||||||
|
onFocused = onFocusedItem,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
rowTestTag = rowTestTag
|
) { item, onFocused, focusModifier ->
|
||||||
) {
|
PosterCardContent(
|
||||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
model = item,
|
||||||
PosterCardContent(
|
onClick = { onMediaSelected(item) },
|
||||||
model = item,
|
onFocused = { onFocused(item) },
|
||||||
onClick = { onMediaSelected(item) },
|
posterWidth = TvHomePosterCardWidth,
|
||||||
posterWidth = TvHomePosterCardWidth,
|
showSecondaryText = true,
|
||||||
contentScale = TvHomeMediaCardScale,
|
indicatorSize = TvHomePosterIndicatorSize,
|
||||||
showSecondaryText = true,
|
indicatorPadding = TvHomePosterIndicatorPadding,
|
||||||
indicatorSize = TvHomePosterIndicatorSize,
|
imageModifier = focusModifier,
|
||||||
indicatorPadding = TvHomePosterIndicatorPadding,
|
focusedScale = 1.07f,
|
||||||
imageModifier = Modifier
|
focusedBorderWidth = 2.dp
|
||||||
.then(
|
)
|
||||||
if (index == 0 && firstItemFocusRequester != null) {
|
|
||||||
Modifier.focusRequester(firstItemFocusRequester)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (index == 0 && firstItemTestTag != null) {
|
|
||||||
Modifier.testTag(firstItemTestTag)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
),
|
|
||||||
onFocused = { onFocusedItem(item) },
|
|
||||||
focusedScale = 1.07f,
|
|
||||||
focusedBorderWidth = 2.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TvHomeSectionRow(
|
private fun TvHomeSectionRow(
|
||||||
|
items: List<MediaUiModel>,
|
||||||
|
onFocused: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
rowTestTag: String? = null,
|
childComponent: @Composable (
|
||||||
content: LazyListScope.() -> Unit,
|
item: MediaUiModel,
|
||||||
|
onFocused: (MediaUiModel) -> Unit,
|
||||||
|
focusModifier: Modifier,
|
||||||
|
) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
var focusedItemId by rememberSaveable { mutableStateOf(items.first().id) }
|
||||||
|
val rowScopedFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
CompositionLocalProvider(LocalBringIntoViewSpec provides TvHomeRowBringIntoViewSpec) {
|
CompositionLocalProvider(LocalBringIntoViewSpec provides TvHomeRowBringIntoViewSpec) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
|
state = listState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(
|
.focusRestorer(rowScopedFocusRequester)
|
||||||
if (rowTestTag != null) {
|
.focusGroup(),
|
||||||
Modifier.testTag(rowTestTag)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
),
|
|
||||||
contentPadding = PaddingValues(horizontal = TvHomeSectionsHorizontalPadding),
|
contentPadding = PaddingValues(horizontal = TvHomeSectionsHorizontalPadding),
|
||||||
horizontalArrangement = Arrangement.spacedBy(TvHomeSectionsRowSpacing),
|
horizontalArrangement = Arrangement.spacedBy(TvHomeSectionsRowSpacing)
|
||||||
content = content
|
) {
|
||||||
)
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
|
val focusModifier =
|
||||||
|
if (item.id == focusedItemId) Modifier.focusRequester(rowScopedFocusRequester) else Modifier
|
||||||
|
childComponent(
|
||||||
|
item,
|
||||||
|
{
|
||||||
|
focusedItemId = item.id
|
||||||
|
onFocused(item)
|
||||||
|
},
|
||||||
|
focusModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +251,10 @@ private fun TvHomeLandscapeCard(
|
|||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.055f else 1f, label = "tv-home-landscape-scale")
|
val scale by animateFloatAsState(
|
||||||
|
targetValue = if (isFocused) 1.055f else 1f,
|
||||||
|
label = "tv-home-landscape-scale"
|
||||||
|
)
|
||||||
val cardWidth = TvHomeLandscapeCardWidth
|
val cardWidth = TvHomeLandscapeCardWidth
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -359,6 +334,7 @@ private fun TvHomeLandscapeCard(
|
|||||||
text = title,
|
text = title,
|
||||||
color = scheme.onBackground,
|
color = scheme.onBackground,
|
||||||
fontSize = TvHomeLandscapeTitleFontSize,
|
fontSize = TvHomeLandscapeTitleFontSize,
|
||||||
|
lineHeight = TvHomeLandscapeTitleFontSize,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
@@ -368,6 +344,7 @@ private fun TvHomeLandscapeCard(
|
|||||||
text = supporting,
|
text = supporting,
|
||||||
color = scheme.onSurfaceVariant,
|
color = scheme.onSurfaceVariant,
|
||||||
fontSize = TvHomeLandscapeSupportingFontSize,
|
fontSize = TvHomeLandscapeSupportingFontSize,
|
||||||
|
lineHeight = TvHomeLandscapeSupportingFontSize,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -218,52 +217,16 @@ private fun TvPlayerBottomSection(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (uiState.activeSkippableSegmentEndMs != null) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
TvIconButton(
|
|
||||||
icon = Icons.Outlined.SkipNext,
|
|
||||||
contentDescription = "Skip segment",
|
|
||||||
onClick = onSkipSegment,
|
|
||||||
size = 64,
|
|
||||||
label = "Skip",
|
|
||||||
modifier = expandPlaylistModifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = formatTime(uiState.positionMs),
|
text = formatTime(uiState.positionMs),
|
||||||
color = scheme.onSurface,
|
color = scheme.onSurface,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
if (uiState.isLive) {
|
Text(
|
||||||
Row(
|
text = formatTime(uiState.durationMs),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
color = scheme.onSurface,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
style = MaterialTheme.typography.bodyMedium
|
||||||
) {
|
)
|
||||||
Text(
|
|
||||||
text = "LIVE",
|
|
||||||
color = scheme.primary,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Catch up",
|
|
||||||
color = scheme.onSurface,
|
|
||||||
modifier = Modifier.clickable { onSeekLiveEdge() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = formatTime(uiState.durationMs),
|
|
||||||
color = scheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TvPlayerSeekBar(
|
TvPlayerSeekBar(
|
||||||
positionMs = uiState.positionMs,
|
positionMs = uiState.positionMs,
|
||||||
@@ -330,7 +293,7 @@ private fun TvPlayerBottomSection(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.align(Alignment.CenterEnd),
|
modifier = Modifier.align(Alignment.CenterStart),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -356,6 +319,22 @@ private fun TvPlayerBottomSection(
|
|||||||
.testTag(TvPlayerSubtitlesButtonTag)
|
.testTag(TvPlayerSubtitlesButtonTag)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (uiState.activeSkippableSegmentEndMs != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TvIconButton(
|
||||||
|
icon = Icons.Outlined.SkipNext,
|
||||||
|
contentDescription = "Skip segment",
|
||||||
|
onClick = onSkipSegment,
|
||||||
|
size = 64,
|
||||||
|
label = "Skip",
|
||||||
|
modifier = expandPlaylistModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isPlaylistExpanded,
|
visible = isPlaylistExpanded,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import java.util.UUID
|
|||||||
@Composable
|
@Composable
|
||||||
fun TvSeriesScreen(
|
fun TvSeriesScreen(
|
||||||
series: SeriesDto,
|
series: SeriesDto,
|
||||||
|
focusedEpisodeId: UUID? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SeriesViewModel = hiltViewModel()
|
viewModel: SeriesViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
@@ -51,6 +52,7 @@ fun TvSeriesScreen(
|
|||||||
TvSeriesScreenContent(
|
TvSeriesScreenContent(
|
||||||
series = seriesData,
|
series = seriesData,
|
||||||
onPlayEpisode = viewModel::onPlayEpisode,
|
onPlayEpisode = viewModel::onPlayEpisode,
|
||||||
|
focusedEpisodeId = focusedEpisodeId,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -62,16 +64,25 @@ fun TvSeriesScreen(
|
|||||||
internal fun TvSeriesScreenContent(
|
internal fun TvSeriesScreenContent(
|
||||||
series: Series,
|
series: Series,
|
||||||
onPlayEpisode: (UUID) -> Unit,
|
onPlayEpisode: (UUID) -> Unit,
|
||||||
|
focusedEpisodeId: UUID? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val nextUpEpisode = remember(series.id) { series.nextUpEpisode() }
|
val nextUpEpisode = remember(series.id) { series.nextUpEpisode() }
|
||||||
var selectedSeason by remember(series.id, nextUpEpisode?.seasonId) {
|
val focusedEpisode = remember(series.id, focusedEpisodeId) {
|
||||||
mutableStateOf(series.defaultSeason(nextUpEpisode))
|
focusedEpisodeId?.let { id ->
|
||||||
|
series.seasons
|
||||||
|
.flatMap { it.episodes }
|
||||||
|
.firstOrNull { it.id == id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val initialEpisode = focusedEpisode ?: nextUpEpisode
|
||||||
|
var selectedSeason by remember(series.id, initialEpisode?.seasonId) {
|
||||||
|
mutableStateOf(series.defaultSeason(initialEpisode))
|
||||||
}
|
}
|
||||||
val firstContentFocusRequester = remember { FocusRequester() }
|
val firstContentFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(series.id, nextUpEpisode?.id) {
|
LaunchedEffect(series.id, initialEpisode?.id) {
|
||||||
if (nextUpEpisode != null) return@LaunchedEffect
|
if (initialEpisode != null) return@LaunchedEffect
|
||||||
withFrameNanos { }
|
withFrameNanos { }
|
||||||
firstContentFocusRequester.requestFocus()
|
firstContentFocusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
@@ -107,7 +118,7 @@ internal fun TvSeriesScreenContent(
|
|||||||
TvEpisodeCarousel(
|
TvEpisodeCarousel(
|
||||||
episodes = selectedSeason.episodes,
|
episodes = selectedSeason.episodes,
|
||||||
onPlayEpisode = { onPlayEpisode(it.id) },
|
onPlayEpisode = { onPlayEpisode(it.id) },
|
||||||
focusedEpisodeId = nextUpEpisode?.id,
|
focusedEpisodeId = initialEpisode?.id,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ class TvFocusedItemHeroTest {
|
|||||||
return Episode(
|
return Episode(
|
||||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||||
seriesId = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
seriesId = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
||||||
|
seriesName = "Love, Death & Robots",
|
||||||
seasonId = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
seasonId = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
||||||
|
seasonIndex = 3,
|
||||||
index = 3,
|
index = 3,
|
||||||
title = "The Very Pulse of the Machine",
|
title = "The Very Pulse of the Machine",
|
||||||
synopsis = "An astronaut faces a strange world alone.",
|
synopsis = "An astronaut faces a strange world alone.",
|
||||||
|
|||||||
@@ -126,7 +126,9 @@ class TvHomeHeroStateTest {
|
|||||||
episode = Episode(
|
episode = Episode(
|
||||||
id = UUID.fromString(id),
|
id = UUID.fromString(id),
|
||||||
seriesId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
seriesId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||||
|
seriesName = "Sample Series",
|
||||||
seasonId = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
seasonId = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||||
|
seasonIndex = 1,
|
||||||
index = 1,
|
index = 1,
|
||||||
title = "Next Episode",
|
title = "Next Episode",
|
||||||
synopsis = "Episode synopsis",
|
synopsis = "Episode synopsis",
|
||||||
|
|||||||
@@ -237,7 +237,9 @@ private fun previewEpisode(): Episode {
|
|||||||
return Episode(
|
return Episode(
|
||||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Severance",
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = 2,
|
||||||
index = 4,
|
index = 4,
|
||||||
title = "The You You Are",
|
title = "The You You Are",
|
||||||
synopsis = "Mark is pulled deeper into Lumon's fractured world as the team chases a clue that reframes everything they thought they understood.",
|
synopsis = "Mark is pulled deeper into Lumon's fractured world as the team chases a clue that reframes everything they thought they understood.",
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ private fun previewSeries(): Series {
|
|||||||
Episode(
|
Episode(
|
||||||
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"),
|
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Constellation",
|
||||||
seasonId = seasonOneId,
|
seasonId = seasonOneId,
|
||||||
|
seasonIndex = 1,
|
||||||
index = 1,
|
index = 1,
|
||||||
title = "A Fresh Start",
|
title = "A Fresh Start",
|
||||||
synopsis = "A fractured crew tries to reassemble after a year apart.",
|
synopsis = "A fractured crew tries to reassemble after a year apart.",
|
||||||
@@ -284,7 +286,9 @@ private fun previewSeries(): Series {
|
|||||||
Episode(
|
Episode(
|
||||||
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2"),
|
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Constellation",
|
||||||
seasonId = seasonOneId,
|
seasonId = seasonOneId,
|
||||||
|
seasonIndex = 1,
|
||||||
index = 2,
|
index = 2,
|
||||||
title = "Signals",
|
title = "Signals",
|
||||||
synopsis = "Anomalies around the station point to a cover-up.",
|
synopsis = "Anomalies around the station point to a cover-up.",
|
||||||
@@ -302,7 +306,9 @@ private fun previewSeries(): Series {
|
|||||||
Episode(
|
Episode(
|
||||||
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa3"),
|
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa3"),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = "Constellation",
|
||||||
seasonId = seasonTwoId,
|
seasonId = seasonTwoId,
|
||||||
|
seasonIndex = 2,
|
||||||
index = 1,
|
index = 1,
|
||||||
title = "Return Window",
|
title = "Return Window",
|
||||||
synopsis = "A high-risk jump changes the rules of the mission.",
|
synopsis = "A high-risk jump changes the rules of the mission.",
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.hilt) apply false
|
alias(libs.plugins.hilt) apply false
|
||||||
|
alias(libs.plugins.google.gms.google.services) apply false
|
||||||
|
alias(libs.plugins.google.firebase.crashlytics) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import java.util.UUID
|
|||||||
data class Episode(
|
data class Episode(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val seriesId: UUID,
|
val seriesId: UUID,
|
||||||
|
val seriesName: String,
|
||||||
val seasonId: UUID,
|
val seasonId: UUID,
|
||||||
|
val seasonIndex: Int,
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val synopsis: String,
|
val synopsis: String,
|
||||||
|
|||||||
@@ -171,7 +171,11 @@ class AppViewModel @Inject constructor(
|
|||||||
when (mediaUiModel) {
|
when (mediaUiModel) {
|
||||||
is MovieUiModel -> onMovieSelected(mediaUiModel.id)
|
is MovieUiModel -> onMovieSelected(mediaUiModel.id)
|
||||||
is SeriesUiModel -> onSeriesSelected(mediaUiModel.id)
|
is SeriesUiModel -> onSeriesSelected(mediaUiModel.id)
|
||||||
is EpisodeUiModel -> onSeriesSelected(mediaUiModel.seriesId)
|
is EpisodeUiModel -> onEpisodeSelected(
|
||||||
|
seriesId = mediaUiModel.seriesId,
|
||||||
|
seasonId = mediaUiModel.seasonId,
|
||||||
|
episodeId = mediaUiModel.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class PlayerViewModel @Inject constructor(
|
|||||||
val controlsVisible: StateFlow<Boolean> = _controlsVisible.asStateFlow()
|
val controlsVisible: StateFlow<Boolean> = _controlsVisible.asStateFlow()
|
||||||
|
|
||||||
private val controlsAutoHidePolicy = ControlsAutoHidePolicy(DEFAULT_CONTROLS_AUTO_HIDE_MS)
|
private val controlsAutoHidePolicy = ControlsAutoHidePolicy(DEFAULT_CONTROLS_AUTO_HIDE_MS)
|
||||||
|
private val seekByCollector = SeekByCollector(viewModelScope) { deltaMs ->
|
||||||
|
playerManager.seekBy(deltaMs)
|
||||||
|
}
|
||||||
private var autoHideJob: Job? = null
|
private var autoHideJob: Job? = null
|
||||||
private var dataErrorMessage: String? = null
|
private var dataErrorMessage: String? = null
|
||||||
|
|
||||||
@@ -191,27 +194,24 @@ class PlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun seekTo(positionMs: Long) {
|
fun seekTo(positionMs: Long) {
|
||||||
|
seekByCollector.clear()
|
||||||
playerManager.seekTo(positionMs)
|
playerManager.seekTo(positionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekBy(deltaMs: Long) {
|
fun seekBy(deltaMs: Long) {
|
||||||
playerManager.seekBy(deltaMs)
|
seekByCollector.seekBySoon(deltaMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekToLiveEdge() {
|
fun seekToLiveEdge() {
|
||||||
|
seekByCollector.clear()
|
||||||
playerManager.seekToLiveEdge()
|
playerManager.seekToLiveEdge()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun skipActiveSegment() {
|
fun skipActiveSegment() {
|
||||||
|
seekByCollector.clear()
|
||||||
playerManager.skipActiveSegment()
|
playerManager.skipActiveSegment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setControlsAutoHideDelay(autoHideDelayMs: Long) {
|
|
||||||
applyControlsAutoHideCommand(
|
|
||||||
controlsAutoHidePolicy.setAutoHideDelay(autoHideDelayMs)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setControlsAutoHideBlocked(
|
fun setControlsAutoHideBlocked(
|
||||||
blocker: ControlsAutoHideBlocker,
|
blocker: ControlsAutoHideBlocker,
|
||||||
blocked: Boolean
|
blocked: Boolean
|
||||||
@@ -246,11 +246,13 @@ class PlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun next(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
fun next(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||||
|
seekByCollector.clear()
|
||||||
playerManager.next()
|
playerManager.next()
|
||||||
showControls(autoHideDelayMs)
|
showControls(autoHideDelayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun previous(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
fun previous(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||||
|
seekByCollector.clear()
|
||||||
playerManager.previous()
|
playerManager.previous()
|
||||||
showControls(autoHideDelayMs)
|
showControls(autoHideDelayMs)
|
||||||
}
|
}
|
||||||
@@ -264,6 +266,7 @@ class PlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun playQueueItem(id: String) {
|
fun playQueueItem(id: String) {
|
||||||
|
seekByCollector.clear()
|
||||||
playerManager.play(id.toUuidOrNull() ?: return)
|
playerManager.play(id.toUuidOrNull() ?: return)
|
||||||
showControls()
|
showControls()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package hu.bbara.purefin.player.viewmodel
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SeekByCollector(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val intervalMs: Long = 120,
|
||||||
|
private val seekBy: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
private var pendingSeekMs = 0L
|
||||||
|
private var flushJob: Job? = null
|
||||||
|
|
||||||
|
fun seekBySoon(deltaMs: Long) {
|
||||||
|
pendingSeekMs += deltaMs
|
||||||
|
if (flushJob?.isActive != true) {
|
||||||
|
scheduleFlush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
pendingSeekMs = 0
|
||||||
|
flushJob?.cancel()
|
||||||
|
flushJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleFlush() {
|
||||||
|
flushJob = scope.launch {
|
||||||
|
delay(intervalMs)
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flush() {
|
||||||
|
val deltaMs = pendingSeekMs
|
||||||
|
pendingSeekMs = 0
|
||||||
|
if (deltaMs != 0L) {
|
||||||
|
seekBy(deltaMs)
|
||||||
|
}
|
||||||
|
if (pendingSeekMs != 0L) {
|
||||||
|
scheduleFlush()
|
||||||
|
} else {
|
||||||
|
flushJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,8 +129,8 @@ class EpisodeUiModel : MediaUiModel {
|
|||||||
|
|
||||||
constructor(episode: Episode) {
|
constructor(episode: Episode) {
|
||||||
id = episode.id
|
id = episode.id
|
||||||
primaryText = episode.title
|
primaryText = episode.seriesName
|
||||||
secondaryText = episode.releaseDate
|
secondaryText = "${episode.seasonIndex} x ${episode.index} : ${episode.title}"
|
||||||
description = episode.synopsis
|
description = episode.synopsis
|
||||||
prefixImageUrl = episode.imageUrlPrefix
|
prefixImageUrl = episode.imageUrlPrefix
|
||||||
progress = (episode.progress?.toFloat() ?: 0f) / 100f
|
progress = (episode.progress?.toFloat() ?: 0f) / 100f
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
|||||||
return Episode(
|
return Episode(
|
||||||
id = id,
|
id = id,
|
||||||
seriesId = seriesId!!,
|
seriesId = seriesId!!,
|
||||||
|
seriesName = seriesName!!,
|
||||||
seasonId = parentId!!,
|
seasonId = parentId!!,
|
||||||
|
seasonIndex = parentIndexNumber!!,
|
||||||
title = name ?: "Unknown title",
|
title = name ?: "Unknown title",
|
||||||
index = indexNumber!!,
|
index = indexNumber!!,
|
||||||
releaseDate = releaseDate,
|
releaseDate = releaseDate,
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ data class CachedSeries(
|
|||||||
data class CachedEpisode(
|
data class CachedEpisode(
|
||||||
val id: String,
|
val id: String,
|
||||||
val seriesId: String,
|
val seriesId: String,
|
||||||
|
val seriesName: String,
|
||||||
val seasonId: String,
|
val seasonId: String,
|
||||||
|
val seasonIndex: Int,
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val synopsis: String,
|
val synopsis: String,
|
||||||
@@ -208,7 +210,9 @@ fun CachedSeries.toSeries(): Series? {
|
|||||||
fun Episode.toCachedEpisode() = CachedEpisode(
|
fun Episode.toCachedEpisode() = CachedEpisode(
|
||||||
id = id.toString(),
|
id = id.toString(),
|
||||||
seriesId = seriesId.toString(),
|
seriesId = seriesId.toString(),
|
||||||
|
seriesName = seriesName,
|
||||||
seasonId = seasonId.toString(),
|
seasonId = seasonId.toString(),
|
||||||
|
seasonIndex = seasonIndex,
|
||||||
index = index,
|
index = index,
|
||||||
title = title,
|
title = title,
|
||||||
synopsis = synopsis,
|
synopsis = synopsis,
|
||||||
@@ -229,7 +233,9 @@ fun CachedEpisode.toEpisode(): Episode? {
|
|||||||
return Episode(
|
return Episode(
|
||||||
id = episodeId,
|
id = episodeId,
|
||||||
seriesId = seriesUuid,
|
seriesId = seriesUuid,
|
||||||
|
seriesName = seriesName,
|
||||||
seasonId = seasonUuid,
|
seasonId = seasonUuid,
|
||||||
|
seasonIndex = seasonIndex,
|
||||||
index = index,
|
index = index,
|
||||||
title = title,
|
title = title,
|
||||||
synopsis = synopsis,
|
synopsis = synopsis,
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import java.util.UUID
|
|||||||
data class EpisodeEntity(
|
data class EpisodeEntity(
|
||||||
@PrimaryKey val id: UUID,
|
@PrimaryKey val id: UUID,
|
||||||
val seriesId: UUID,
|
val seriesId: UUID,
|
||||||
|
val seriesName: String,
|
||||||
val seasonId: UUID,
|
val seasonId: UUID,
|
||||||
|
val seasonIndex: Int,
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val synopsis: String,
|
val synopsis: String,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import hu.bbara.purefin.data.offline.room.entity.SmartDownloadEntity
|
|||||||
EpisodeEntity::class,
|
EpisodeEntity::class,
|
||||||
SmartDownloadEntity::class,
|
SmartDownloadEntity::class,
|
||||||
],
|
],
|
||||||
version = 10,
|
version = 11,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(UuidConverters::class)
|
@TypeConverters(UuidConverters::class)
|
||||||
|
|||||||
@@ -246,7 +246,9 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
private fun Episode.toEntity() = EpisodeEntity(
|
private fun Episode.toEntity() = EpisodeEntity(
|
||||||
id = id,
|
id = id,
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = seriesName,
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = seasonIndex,
|
||||||
index = index,
|
index = index,
|
||||||
title = title,
|
title = title,
|
||||||
synopsis = synopsis,
|
synopsis = synopsis,
|
||||||
@@ -300,7 +302,9 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
private fun EpisodeEntity.toDomain() = Episode(
|
private fun EpisodeEntity.toDomain() = Episode(
|
||||||
id = id,
|
id = id,
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
seriesName = seriesName,
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
seasonIndex = seasonIndex,
|
||||||
index = index,
|
index = index,
|
||||||
title = title,
|
title = title,
|
||||||
synopsis = synopsis,
|
synopsis = synopsis,
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ media3FfmpegDecoder = "1.9.0+1"
|
|||||||
nav3Core = "1.0.0"
|
nav3Core = "1.0.0"
|
||||||
room = "2.7.0"
|
room = "2.7.0"
|
||||||
slf4j = "2.0.17"
|
slf4j = "2.0.17"
|
||||||
|
firebaseCrashlytics = "20.0.5"
|
||||||
|
googleGmsGoogleServices = "4.4.4"
|
||||||
|
googleFirebaseCrashlytics = "3.0.7"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -69,6 +72,7 @@ androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", vers
|
|||||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||||
|
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
@@ -80,3 +84,5 @@ kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "ko
|
|||||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" }
|
||||||
|
google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "googleFirebaseCrashlytics" }
|
||||||
|
|||||||
Reference in New Issue
Block a user