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
|
||||
.codex/
|
||||
openspec
|
||||
*.patch
|
||||
|
||||
@@ -7,6 +7,8 @@ plugins {
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.google.gms.google.services)
|
||||
alias(libs.plugins.google.firebase.crashlytics)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -82,6 +84,7 @@ dependencies {
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
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(
|
||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Severance",
|
||||
seasonId = seasonId,
|
||||
seasonIndex = 2,
|
||||
index = 4,
|
||||
title = "The You You Are",
|
||||
synopsis = "Mark is pulled deeper into Lumon's fractured world as the team chases a clue.",
|
||||
|
||||
@@ -425,7 +425,9 @@ class TvHomeContentTest {
|
||||
return Episode(
|
||||
id = UUID.fromString(id),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Firefly",
|
||||
seasonId = seasonId,
|
||||
seasonIndex = 1,
|
||||
index = 1,
|
||||
title = title,
|
||||
synopsis = "A crew member takes the shuttle for a spin and makes a mess.",
|
||||
|
||||
@@ -120,7 +120,9 @@ class SeriesScreenContentTest {
|
||||
Episode(
|
||||
id = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Severance",
|
||||
seasonId = seasonId,
|
||||
seasonIndex = 1,
|
||||
index = 1,
|
||||
title = "Good News About Hell",
|
||||
synopsis = "Mark is promoted after an unexpected tragedy.",
|
||||
@@ -136,7 +138,9 @@ class SeriesScreenContentTest {
|
||||
Episode(
|
||||
id = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Severance",
|
||||
seasonId = seasonId,
|
||||
seasonIndex = 1,
|
||||
index = 2,
|
||||
title = "Half Loop",
|
||||
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.navigation3.runtime.EntryProviderScope
|
||||
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.series.TvSeriesScreen
|
||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||
@@ -37,7 +36,10 @@ fun EntryProviderScope<Route>.tvSeriesSection() {
|
||||
|
||||
fun EntryProviderScope<Route>.tvEpisodeSection() {
|
||||
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.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.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||
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
|
||||
|
||||
@Composable
|
||||
@@ -43,7 +43,6 @@ fun PosterCard(
|
||||
modifier: Modifier = Modifier,
|
||||
imageModifier: Modifier = Modifier,
|
||||
posterWidth: Dp = 144.dp,
|
||||
contentScale: Float = 1f,
|
||||
showSecondaryText: Boolean = false,
|
||||
indicatorSize: Int = 28,
|
||||
indicatorPadding: Dp = 8.dp,
|
||||
@@ -65,7 +64,6 @@ fun PosterCard(
|
||||
modifier = modifier,
|
||||
imageModifier = imageModifier,
|
||||
posterWidth = posterWidth,
|
||||
contentScale = contentScale,
|
||||
showSecondaryText = showSecondaryText,
|
||||
indicatorSize = indicatorSize,
|
||||
indicatorPadding = indicatorPadding,
|
||||
@@ -82,11 +80,10 @@ fun PosterCardContent(
|
||||
modifier: Modifier = Modifier,
|
||||
imageModifier: Modifier = Modifier,
|
||||
posterWidth: Dp = 144.dp,
|
||||
contentScale: Float = 1f,
|
||||
showSecondaryText: Boolean = false,
|
||||
indicatorSize: Int = 28,
|
||||
indicatorPadding: Dp = 8.dp,
|
||||
onFocused: () -> Unit = {},
|
||||
onFocused: () -> Unit,
|
||||
focusedScale: Float = 1f,
|
||||
focusedBorderWidth: Dp = 1.dp,
|
||||
focusedTransformOrigin: TransformOrigin = TransformOrigin(0.5f, 0f)
|
||||
@@ -155,6 +152,7 @@ fun PosterCardContent(
|
||||
text = model.primaryText,
|
||||
color = scheme.onBackground,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -167,6 +165,7 @@ fun PosterCardContent(
|
||||
text = text,
|
||||
color = scheme.onSurfaceVariant,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 11.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.compose.foundation.gestures.BringIntoViewSpec
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
|
||||
internal val TvHomeFocusedItemTopOffset = 42.dp
|
||||
internal val TvHomeFocusedItemTopOffset = 48.dp
|
||||
internal val TvHomeBringIntoViewTrailingSpace = 120.dp
|
||||
|
||||
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.gestures.LocalBringIntoViewSpec
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -20,11 +21,11 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
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.MediaUiModel
|
||||
import java.util.UUID
|
||||
|
||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||
@@ -46,9 +47,6 @@ fun TvHomeContent(
|
||||
val hasContinueWatching = continueWatching.isNotEmpty()
|
||||
val hasNextUp = nextUp.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 hasInitialFocusableItem = hasContinueWatching || hasNextUp || hasLibraryItems
|
||||
val initialFocusRequester = remember { FocusRequester() }
|
||||
@@ -68,21 +66,16 @@ fun TvHomeContent(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(scheme.background)
|
||||
.testTag(TvHomeContentViewportTag),
|
||||
.focusRequester(initialFocusRequester),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
contentPadding = contentPadding
|
||||
) {
|
||||
item(key = "tv-home-top-spacer") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
if (hasContinueWatching) {
|
||||
item(key = "tv-home-continue-watching") {
|
||||
TvContinueWatchingSection(
|
||||
items = continueWatching,
|
||||
onFocusedItem = onMediaFocused,
|
||||
onMediaSelected = onMediaSelected,
|
||||
firstItemFocusRequester = initialFocusRequester,
|
||||
firstItemTestTag = TvHomeInitialFocusTag,
|
||||
rowTestTag = TvHomeContinueWatchingRowTag
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,9 +86,6 @@ fun TvHomeContent(
|
||||
items = nextUp,
|
||||
onFocusedItem = onMediaFocused,
|
||||
onMediaSelected = onMediaSelected,
|
||||
firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching },
|
||||
firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching },
|
||||
rowTestTag = TvHomeNextUpRowTag
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,10 +98,6 @@ fun TvHomeContent(
|
||||
title = library.name,
|
||||
items = libraryContent[library.id].orEmpty(),
|
||||
onFocusedItem = onMediaFocused,
|
||||
firstItemFocusRequester = initialFocusRequester.takeIf {
|
||||
!hasContinueWatching && !hasNextUp && library.id == firstLibraryWithItemsId
|
||||
},
|
||||
firstItemTestTag = tvHomeLibraryFirstItemTag(library.id),
|
||||
onMediaSelected = onMediaSelected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,21 +5,24 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -28,26 +31,27 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
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 hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
import hu.bbara.purefin.ui.common.card.PosterCardContent
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import java.util.UUID
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -61,15 +65,14 @@ private val TvHomeLandscapeFocusedBorderWidth = (2f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeUnfocusedBorderWidth = (1f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeProgressBarHeight = (6f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeProgressBarPadding = (8f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataSpacing = (4f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataTopPadding = (12f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataSpacing = (1f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataTopPadding = (6f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataHorizontalPadding = (4f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeTitleFontSize = (15f * TvHomeMediaCardScale).sp
|
||||
private val TvHomeLandscapeSupportingFontSize = (11f * TvHomeMediaCardScale).sp
|
||||
private val TvHomePosterIndicatorSize = (24f * TvHomeMediaCardScale).roundToInt()
|
||||
private val TvHomePosterIndicatorPadding = (6f * TvHomeMediaCardScale).dp
|
||||
internal const val TvHomeSectionRowTagPrefix = "tv-home-section-row-"
|
||||
internal const val TvHomeContinueWatchingRowTag = "${TvHomeSectionRowTagPrefix}continue-watching"
|
||||
internal const val TvHomeNextUpRowTag = "${TvHomeSectionRowTagPrefix}next-up"
|
||||
|
||||
internal fun tvHomeLibraryRowTag(libraryId: UUID): String =
|
||||
@@ -81,168 +84,137 @@ internal fun tvHomeLibraryFirstItemTag(libraryId: UUID): String =
|
||||
@Composable
|
||||
fun TvContinueWatchingSection(
|
||||
items: List<MediaUiModel>,
|
||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||
onFocusedItem: (MediaUiModel) -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
firstItemFocusRequester: FocusRequester? = null,
|
||||
firstItemTestTag: String? = null,
|
||||
rowTestTag: String? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (items.isEmpty()) return
|
||||
TvSectionHeader(
|
||||
title = "Continue Watching",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TvHomeSectionRow(
|
||||
items = items,
|
||||
onFocused = onFocusedItem,
|
||||
modifier = modifier,
|
||||
rowTestTag = rowTestTag
|
||||
) {
|
||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||
) { item, onFocused, focusModifier ->
|
||||
TvHomeLandscapeCard(
|
||||
title = item.primaryText,
|
||||
supporting = item.secondaryText,
|
||||
imageUrl = item.primaryImageUrl,
|
||||
progress = item.progress ?: 0f,
|
||||
imageModifier = Modifier
|
||||
.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) },
|
||||
imageModifier = focusModifier,
|
||||
onFocusedItem = {
|
||||
onFocused(item)
|
||||
},
|
||||
onClick = { onMediaSelected(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvNextUpSection(
|
||||
items: List<MediaUiModel>,
|
||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||
onFocusedItem: (MediaUiModel) -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
firstItemFocusRequester: FocusRequester? = null,
|
||||
firstItemTestTag: String? = null,
|
||||
rowTestTag: String? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (items.isEmpty()) return
|
||||
TvSectionHeader(
|
||||
title = "Next Up",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TvHomeSectionRow(
|
||||
items = items,
|
||||
onFocused = onFocusedItem,
|
||||
modifier = modifier,
|
||||
rowTestTag = rowTestTag
|
||||
) {
|
||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||
) { item, onFocused, focusModifier ->
|
||||
TvHomeLandscapeCard(
|
||||
title = item.primaryText,
|
||||
supporting = item.secondaryText,
|
||||
imageUrl = item.primaryImageUrl,
|
||||
imageModifier = Modifier
|
||||
.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) },
|
||||
imageModifier = focusModifier,
|
||||
onFocusedItem = { onFocused(item) },
|
||||
onClick = { onMediaSelected(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvLibraryPosterSection(
|
||||
title: String,
|
||||
items: List<MediaUiModel>,
|
||||
onFocusedItem: (MediaUiModel) -> Unit = {},
|
||||
firstItemFocusRequester: FocusRequester? = null,
|
||||
firstItemTestTag: String? = null,
|
||||
rowTestTag: String? = null,
|
||||
onFocusedItem: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
) {
|
||||
if (items.isEmpty()) return
|
||||
TvSectionHeader(
|
||||
title = title,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TvHomeSectionRow(
|
||||
items = items,
|
||||
onFocused = onFocusedItem,
|
||||
modifier = modifier,
|
||||
rowTestTag = rowTestTag
|
||||
) {
|
||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||
) { item, onFocused, focusModifier ->
|
||||
PosterCardContent(
|
||||
model = item,
|
||||
onClick = { onMediaSelected(item) },
|
||||
onFocused = { onFocused(item) },
|
||||
posterWidth = TvHomePosterCardWidth,
|
||||
contentScale = TvHomeMediaCardScale,
|
||||
showSecondaryText = true,
|
||||
indicatorSize = TvHomePosterIndicatorSize,
|
||||
indicatorPadding = TvHomePosterIndicatorPadding,
|
||||
imageModifier = Modifier
|
||||
.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) },
|
||||
imageModifier = focusModifier,
|
||||
focusedScale = 1.07f,
|
||||
focusedBorderWidth = 2.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun TvHomeSectionRow(
|
||||
items: List<MediaUiModel>,
|
||||
onFocused: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
rowTestTag: String? = null,
|
||||
content: LazyListScope.() -> Unit,
|
||||
childComponent: @Composable (
|
||||
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) {
|
||||
LazyRow(
|
||||
state = listState,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (rowTestTag != null) {
|
||||
Modifier.testTag(rowTestTag)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
.focusRestorer(rowScopedFocusRequester)
|
||||
.focusGroup(),
|
||||
contentPadding = PaddingValues(horizontal = TvHomeSectionsHorizontalPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(TvHomeSectionsRowSpacing),
|
||||
content = content
|
||||
horizontalArrangement = Arrangement.spacedBy(TvHomeSectionsRowSpacing)
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvSectionHeader(
|
||||
@@ -279,7 +251,10 @@ private fun TvHomeLandscapeCard(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
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
|
||||
|
||||
Column(
|
||||
@@ -359,6 +334,7 @@ private fun TvHomeLandscapeCard(
|
||||
text = title,
|
||||
color = scheme.onBackground,
|
||||
fontSize = TvHomeLandscapeTitleFontSize,
|
||||
lineHeight = TvHomeLandscapeTitleFontSize,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -368,6 +344,7 @@ private fun TvHomeLandscapeCard(
|
||||
text = supporting,
|
||||
color = scheme.onSurfaceVariant,
|
||||
fontSize = TvHomeLandscapeSupportingFontSize,
|
||||
lineHeight = TvHomeLandscapeSupportingFontSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -218,53 +217,17 @@ private fun TvPlayerBottomSection(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
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 = formatTime(uiState.positionMs),
|
||||
color = scheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
if (uiState.isLive) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
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(
|
||||
positionMs = uiState.positionMs,
|
||||
durationMs = uiState.durationMs,
|
||||
@@ -330,7 +293,7 @@ private fun TvPlayerBottomSection(
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -356,6 +319,22 @@ private fun TvPlayerBottomSection(
|
||||
.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(
|
||||
visible = isPlaylistExpanded,
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.UUID
|
||||
@Composable
|
||||
fun TvSeriesScreen(
|
||||
series: SeriesDto,
|
||||
focusedEpisodeId: UUID? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SeriesViewModel = hiltViewModel()
|
||||
) {
|
||||
@@ -51,6 +52,7 @@ fun TvSeriesScreen(
|
||||
TvSeriesScreenContent(
|
||||
series = seriesData,
|
||||
onPlayEpisode = viewModel::onPlayEpisode,
|
||||
focusedEpisodeId = focusedEpisodeId,
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
@@ -62,16 +64,25 @@ fun TvSeriesScreen(
|
||||
internal fun TvSeriesScreenContent(
|
||||
series: Series,
|
||||
onPlayEpisode: (UUID) -> Unit,
|
||||
focusedEpisodeId: UUID? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val nextUpEpisode = remember(series.id) { series.nextUpEpisode() }
|
||||
var selectedSeason by remember(series.id, nextUpEpisode?.seasonId) {
|
||||
mutableStateOf(series.defaultSeason(nextUpEpisode))
|
||||
val focusedEpisode = remember(series.id, focusedEpisodeId) {
|
||||
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() }
|
||||
|
||||
LaunchedEffect(series.id, nextUpEpisode?.id) {
|
||||
if (nextUpEpisode != null) return@LaunchedEffect
|
||||
LaunchedEffect(series.id, initialEpisode?.id) {
|
||||
if (initialEpisode != null) return@LaunchedEffect
|
||||
withFrameNanos { }
|
||||
firstContentFocusRequester.requestFocus()
|
||||
}
|
||||
@@ -107,7 +118,7 @@ internal fun TvSeriesScreenContent(
|
||||
TvEpisodeCarousel(
|
||||
episodes = selectedSeason.episodes,
|
||||
onPlayEpisode = { onPlayEpisode(it.id) },
|
||||
focusedEpisodeId = nextUpEpisode?.id,
|
||||
focusedEpisodeId = initialEpisode?.id,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,9 @@ class TvFocusedItemHeroTest {
|
||||
return Episode(
|
||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||
seriesId = UUID.fromString("44444444-4444-4444-4444-444444444444"),
|
||||
seriesName = "Love, Death & Robots",
|
||||
seasonId = UUID.fromString("55555555-5555-5555-5555-555555555555"),
|
||||
seasonIndex = 3,
|
||||
index = 3,
|
||||
title = "The Very Pulse of the Machine",
|
||||
synopsis = "An astronaut faces a strange world alone.",
|
||||
|
||||
@@ -126,7 +126,9 @@ class TvHomeHeroStateTest {
|
||||
episode = Episode(
|
||||
id = UUID.fromString(id),
|
||||
seriesId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
seriesName = "Sample Series",
|
||||
seasonId = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
seasonIndex = 1,
|
||||
index = 1,
|
||||
title = "Next Episode",
|
||||
synopsis = "Episode synopsis",
|
||||
|
||||
@@ -237,7 +237,9 @@ private fun previewEpisode(): Episode {
|
||||
return Episode(
|
||||
id = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Severance",
|
||||
seasonId = seasonId,
|
||||
seasonIndex = 2,
|
||||
index = 4,
|
||||
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.",
|
||||
|
||||
@@ -268,7 +268,9 @@ private fun previewSeries(): Series {
|
||||
Episode(
|
||||
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Constellation",
|
||||
seasonId = seasonOneId,
|
||||
seasonIndex = 1,
|
||||
index = 1,
|
||||
title = "A Fresh Start",
|
||||
synopsis = "A fractured crew tries to reassemble after a year apart.",
|
||||
@@ -284,7 +286,9 @@ private fun previewSeries(): Series {
|
||||
Episode(
|
||||
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Constellation",
|
||||
seasonId = seasonOneId,
|
||||
seasonIndex = 1,
|
||||
index = 2,
|
||||
title = "Signals",
|
||||
synopsis = "Anomalies around the station point to a cover-up.",
|
||||
@@ -302,7 +306,9 @@ private fun previewSeries(): Series {
|
||||
Episode(
|
||||
id = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa3"),
|
||||
seriesId = seriesId,
|
||||
seriesName = "Constellation",
|
||||
seasonId = seasonTwoId,
|
||||
seasonIndex = 2,
|
||||
index = 1,
|
||||
title = "Return Window",
|
||||
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.ksp) 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(
|
||||
val id: UUID,
|
||||
val seriesId: UUID,
|
||||
val seriesName: String,
|
||||
val seasonId: UUID,
|
||||
val seasonIndex: Int,
|
||||
val index: Int,
|
||||
val title: String,
|
||||
val synopsis: String,
|
||||
|
||||
@@ -171,7 +171,11 @@ class AppViewModel @Inject constructor(
|
||||
when (mediaUiModel) {
|
||||
is MovieUiModel -> onMovieSelected(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()
|
||||
|
||||
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 dataErrorMessage: String? = null
|
||||
|
||||
@@ -191,27 +194,24 @@ class PlayerViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun seekTo(positionMs: Long) {
|
||||
seekByCollector.clear()
|
||||
playerManager.seekTo(positionMs)
|
||||
}
|
||||
|
||||
fun seekBy(deltaMs: Long) {
|
||||
playerManager.seekBy(deltaMs)
|
||||
seekByCollector.seekBySoon(deltaMs)
|
||||
}
|
||||
|
||||
fun seekToLiveEdge() {
|
||||
seekByCollector.clear()
|
||||
playerManager.seekToLiveEdge()
|
||||
}
|
||||
|
||||
fun skipActiveSegment() {
|
||||
seekByCollector.clear()
|
||||
playerManager.skipActiveSegment()
|
||||
}
|
||||
|
||||
fun setControlsAutoHideDelay(autoHideDelayMs: Long) {
|
||||
applyControlsAutoHideCommand(
|
||||
controlsAutoHidePolicy.setAutoHideDelay(autoHideDelayMs)
|
||||
)
|
||||
}
|
||||
|
||||
fun setControlsAutoHideBlocked(
|
||||
blocker: ControlsAutoHideBlocker,
|
||||
blocked: Boolean
|
||||
@@ -246,11 +246,13 @@ class PlayerViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun next(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||
seekByCollector.clear()
|
||||
playerManager.next()
|
||||
showControls(autoHideDelayMs)
|
||||
}
|
||||
|
||||
fun previous(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||
seekByCollector.clear()
|
||||
playerManager.previous()
|
||||
showControls(autoHideDelayMs)
|
||||
}
|
||||
@@ -264,6 +266,7 @@ class PlayerViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun playQueueItem(id: String) {
|
||||
seekByCollector.clear()
|
||||
playerManager.play(id.toUuidOrNull() ?: return)
|
||||
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) {
|
||||
id = episode.id
|
||||
primaryText = episode.title
|
||||
secondaryText = episode.releaseDate
|
||||
primaryText = episode.seriesName
|
||||
secondaryText = "${episode.seasonIndex} x ${episode.index} : ${episode.title}"
|
||||
description = episode.synopsis
|
||||
prefixImageUrl = episode.imageUrlPrefix
|
||||
progress = (episode.progress?.toFloat() ?: 0f) / 100f
|
||||
|
||||
@@ -95,7 +95,9 @@ fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
return Episode(
|
||||
id = id,
|
||||
seriesId = seriesId!!,
|
||||
seriesName = seriesName!!,
|
||||
seasonId = parentId!!,
|
||||
seasonIndex = parentIndexNumber!!,
|
||||
title = name ?: "Unknown title",
|
||||
index = indexNumber!!,
|
||||
releaseDate = releaseDate,
|
||||
|
||||
@@ -50,7 +50,9 @@ data class CachedSeries(
|
||||
data class CachedEpisode(
|
||||
val id: String,
|
||||
val seriesId: String,
|
||||
val seriesName: String,
|
||||
val seasonId: String,
|
||||
val seasonIndex: Int,
|
||||
val index: Int,
|
||||
val title: String,
|
||||
val synopsis: String,
|
||||
@@ -208,7 +210,9 @@ fun CachedSeries.toSeries(): Series? {
|
||||
fun Episode.toCachedEpisode() = CachedEpisode(
|
||||
id = id.toString(),
|
||||
seriesId = seriesId.toString(),
|
||||
seriesName = seriesName,
|
||||
seasonId = seasonId.toString(),
|
||||
seasonIndex = seasonIndex,
|
||||
index = index,
|
||||
title = title,
|
||||
synopsis = synopsis,
|
||||
@@ -229,7 +233,9 @@ fun CachedEpisode.toEpisode(): Episode? {
|
||||
return Episode(
|
||||
id = episodeId,
|
||||
seriesId = seriesUuid,
|
||||
seriesName = seriesName,
|
||||
seasonId = seasonUuid,
|
||||
seasonIndex = seasonIndex,
|
||||
index = index,
|
||||
title = title,
|
||||
synopsis = synopsis,
|
||||
|
||||
@@ -21,7 +21,9 @@ import java.util.UUID
|
||||
data class EpisodeEntity(
|
||||
@PrimaryKey val id: UUID,
|
||||
val seriesId: UUID,
|
||||
val seriesName: String,
|
||||
val seasonId: UUID,
|
||||
val seasonIndex: Int,
|
||||
val index: Int,
|
||||
val title: String,
|
||||
val synopsis: String,
|
||||
|
||||
@@ -23,7 +23,7 @@ import hu.bbara.purefin.data.offline.room.entity.SmartDownloadEntity
|
||||
EpisodeEntity::class,
|
||||
SmartDownloadEntity::class,
|
||||
],
|
||||
version = 10,
|
||||
version = 11,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(UuidConverters::class)
|
||||
|
||||
@@ -246,7 +246,9 @@ class OfflineRoomMediaLocalDataSource(
|
||||
private fun Episode.toEntity() = EpisodeEntity(
|
||||
id = id,
|
||||
seriesId = seriesId,
|
||||
seriesName = seriesName,
|
||||
seasonId = seasonId,
|
||||
seasonIndex = seasonIndex,
|
||||
index = index,
|
||||
title = title,
|
||||
synopsis = synopsis,
|
||||
@@ -300,7 +302,9 @@ class OfflineRoomMediaLocalDataSource(
|
||||
private fun EpisodeEntity.toDomain() = Episode(
|
||||
id = id,
|
||||
seriesId = seriesId,
|
||||
seriesName = seriesName,
|
||||
seasonId = seasonId,
|
||||
seasonIndex = seasonIndex,
|
||||
index = index,
|
||||
title = title,
|
||||
synopsis = synopsis,
|
||||
|
||||
@@ -24,6 +24,9 @@ media3FfmpegDecoder = "1.9.0+1"
|
||||
nav3Core = "1.0.0"
|
||||
room = "2.7.0"
|
||||
slf4j = "2.0.17"
|
||||
firebaseCrashlytics = "20.0.5"
|
||||
googleGmsGoogleServices = "4.4.4"
|
||||
googleFirebaseCrashlytics = "3.0.7"
|
||||
|
||||
[libraries]
|
||||
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-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
|
||||
|
||||
|
||||
[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" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
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