diff --git a/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt index 2896f72c01..0c9c17f3d3 100644 --- a/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt +++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,6 +29,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -43,6 +43,7 @@ fun PodcastImage( contentDescription: String?, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Crop, + placeholderBrush: Brush = thumbnailPlaceholderDefaultBrush(), ) { var imagePainterState by remember { mutableStateOf(AsyncImagePainter.State.Empty) @@ -73,8 +74,9 @@ fun PodcastImage( else -> { Box( modifier = Modifier + .background(placeholderBrush) .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerHigh) + ) } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt similarity index 53% rename from Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt rename to Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt index f7ad98cfec..865dac3130 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt +++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt @@ -14,19 +14,30 @@ * limitations under the License. */ -package com.example.jetcaster.tv.ui.component +package com.example.jetcaster.designsystem.component +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.painter.BrushPainter -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.MaterialTheme +import com.example.jetcaster.designsystem.theme.surfaceVariantDark +import com.example.jetcaster.designsystem.theme.surfaceVariantLight -@OptIn(ExperimentalTvMaterial3Api::class) @Composable -internal fun thumbnailPlaceholder( - brush: Brush = SolidColor(MaterialTheme.colorScheme.surfaceVariant) -): BrushPainter { - return BrushPainter(brush) +internal fun thumbnailPlaceholderDefaultBrush( + color: Color = thumbnailPlaceHolderDefaultColor() +): Brush { + return SolidColor(color) +} + +@Composable +private fun thumbnailPlaceHolderDefaultColor( + isInDarkMode: Boolean = isSystemInDarkTheme() +): Color { + return if (isInDarkMode) { + surfaceVariantDark + } else { + surfaceVariantLight + } } diff --git a/Jetcaster/tv-app/build.gradle.kts b/Jetcaster/tv-app/build.gradle.kts index 7448b4364f..4eda835420 100644 --- a/Jetcaster/tv-app/build.gradle.kts +++ b/Jetcaster/tv-app/build.gradle.kts @@ -40,8 +40,10 @@ android { buildTypes { getByName("release") { isMinifyEnabled = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } @@ -79,7 +81,6 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.activity.compose) implementation(libs.androidx.navigation.compose) - implementation(libs.coil.kt.compose) // Dependency injection implementation(libs.androidx.hilt.navigation.compose) @@ -87,7 +88,6 @@ dependencies { implementation(project(":core:model")) ksp(libs.hilt.compiler) - implementation(project(":core")) implementation(project(":designsystem")) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt index 6c623e7fce..5ce9c5ace4 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt @@ -17,9 +17,9 @@ package com.example.jetcaster.tv.model import androidx.compose.runtime.Immutable -import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo +import com.example.jetcaster.core.model.PodcastInfo @Immutable data class PodcastList( - val member: List -) : List by member + val member: List +) : List by member diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt index d5b4f3b257..0ea0a35967 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt @@ -141,7 +141,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { LibraryScreen( navigateToDiscover = jetcasterAppState::navigateToDiscover, showPodcastDetails = { - jetcasterAppState.showPodcastDetails(it.podcast.uri) + jetcasterAppState.showPodcastDetails(it.uri) }, playEpisode = { jetcasterAppState.playEpisode() @@ -156,7 +156,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { composable(Screen.Search.route) { SearchScreen( onPodcastSelected = { - jetcasterAppState.showPodcastDetails(it.podcast.uri) + jetcasterAppState.showPodcastDetails(it.uri) }, modifier = Modifier .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt index 752cbdf3f7..e0104254ab 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt @@ -23,43 +23,54 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.example.jetcaster.core.data.database.model.Podcast import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.designsystem.component.ImageBackgroundRadialGradientScrim @Composable -internal fun Background( - podcast: Podcast, - modifier: Modifier = Modifier, -) = Background(imageUrl = podcast.imageUrl, modifier) - -@Composable -internal fun Background( - episode: PlayerEpisode, +internal fun BackgroundContainer( + playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, -) = Background(imageUrl = episode.podcastImageUrl, modifier) + contentAlignment: Alignment = Alignment.Center, + content: @Composable BoxScope.() -> Unit +) = + BackgroundContainer( + imageUrl = playerEpisode.podcastImageUrl, + modifier, + contentAlignment, + content + ) @Composable -internal fun Background( - imageUrl: String?, +internal fun BackgroundContainer( + podcastInfo: PodcastInfo, modifier: Modifier = Modifier, -) { - ImageBackgroundRadialGradientScrim( - url = imageUrl, - colors = listOf(Color.Black, Color.Transparent), - modifier = modifier, - ) -} + contentAlignment: Alignment = Alignment.Center, + content: @Composable BoxScope.() -> Unit +) = + BackgroundContainer(imageUrl = podcastInfo.imageUrl, modifier, contentAlignment, content) @Composable internal fun BackgroundContainer( - playerEpisode: PlayerEpisode, + imageUrl: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, content: @Composable BoxScope.() -> Unit ) { Box(modifier = modifier, contentAlignment = contentAlignment) { - Background(episode = playerEpisode, modifier = Modifier.fillMaxSize()) + Background(imageUrl = imageUrl, modifier = Modifier.fillMaxSize()) content() } } + +@Composable +private fun Background( + imageUrl: String, + modifier: Modifier = Modifier, +) { + ImageBackgroundRadialGradientScrim( + url = imageUrl, + colors = listOf(Color.Black, Color.Transparent), + modifier = modifier, + ) +} diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt index 1fb1f9b8d4..308b959080 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt @@ -35,8 +35,8 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.R import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList @@ -46,7 +46,7 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults internal fun Catalog( podcastList: PodcastList, latestEpisodeList: EpisodeList, - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, onEpisodeSelected: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, state: TvLazyListState = rememberTvLazyListState(), @@ -83,7 +83,7 @@ internal fun Catalog( @Composable private fun PodcastSection( podcastList: PodcastList, - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, title: String? = null, ) { @@ -142,7 +142,7 @@ private fun Section( @Composable private fun PodcastRow( podcastList: PodcastList, - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), horizontalArrangement: Arrangement.Horizontal = @@ -155,7 +155,7 @@ private fun PodcastRow( ) { items(podcastList) { PodcastCard( - podcast = it.podcast, + podcastInfo = it, onClick = { onPodcastSelected(it) }, modifier = Modifier.width(JetcasterAppDefaults.cardWidth.medium) ) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt index 7b2e22e851..587049fc47 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt @@ -19,7 +19,6 @@ package com.example.jetcaster.tv.ui.component import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -36,7 +35,6 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.WideCardLayout -import coil.compose.AsyncImage import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @@ -78,12 +76,7 @@ private fun EpisodeThumbnail( scale = CardScale.None, modifier = modifier, ) { - AsyncImage( - model = playerEpisode.podcastImageUrl, - contentDescription = null, - placeholder = thumbnailPlaceholder(), - modifier = Modifier.fillMaxSize() - ) + Thumbnail(episode = playerEpisode, size = JetcasterAppDefaults.thumbnailSize.episode) } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt index 1df5a815f0..69c2dfa3a6 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt @@ -17,7 +17,6 @@ package com.example.jetcaster.tv.ui.component import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -26,14 +25,13 @@ import androidx.tv.material3.CardScale import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.StandardCardLayout import androidx.tv.material3.Text -import coil.compose.AsyncImage -import com.example.jetcaster.core.data.database.model.Podcast +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @OptIn(ExperimentalTvMaterial3Api::class) @Composable internal fun PodcastCard( - podcast: Podcast, + podcastInfo: PodcastInfo, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -44,16 +42,14 @@ internal fun PodcastCard( interactionSource = it, scale = CardScale.None, ) { - AsyncImage( - model = podcast.imageUrl, - contentDescription = null, - placeholder = thumbnailPlaceholder(), - modifier = Modifier.size(JetcasterAppDefaults.thumbnailSize.podcast) + Thumbnail( + podcastInfo = podcastInfo, + size = JetcasterAppDefaults.thumbnailSize.podcast ) } }, title = { - Text(text = podcast.title, modifier = Modifier.padding(top = 12.dp)) + Text(text = podcastInfo.title, modifier = Modifier.padding(top = 12.dp)) }, modifier = modifier, ) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt index 88b37b49d6..5f16fcd4e6 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt @@ -24,14 +24,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.example.jetcaster.core.data.database.model.Podcast import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.designsystem.component.PodcastImage import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @Composable fun Thumbnail( - podcast: Podcast, + podcastInfo: PodcastInfo, modifier: Modifier = Modifier, shape: RoundedCornerShape = RoundedCornerShape(12.dp), size: DpSize = DpSize( @@ -41,7 +41,7 @@ fun Thumbnail( contentScale: ContentScale = ContentScale.Crop ) = Thumbnail( - podcast.imageUrl, + podcastInfo.imageUrl, modifier, shape, size, @@ -69,7 +69,7 @@ fun Thumbnail( @Composable fun Thumbnail( - url: String?, + url: String, modifier: Modifier = Modifier, shape: RoundedCornerShape = RoundedCornerShape(12.dp), size: DpSize = DpSize( @@ -78,12 +78,11 @@ fun Thumbnail( ), contentScale: ContentScale = ContentScale.Crop ) = - AsyncImage( - model = url, + PodcastImage( + podcastImageUrl = url, contentDescription = null, contentScale = contentScale, - modifier = Modifier - .size(size) + modifier = modifier .clip(shape) - .then(modifier) + .size(size), ) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt index 627f9e1aa7..f1b058f0d7 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt @@ -36,10 +36,9 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Tab import androidx.tv.material3.TabRow import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.Podcast -import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.model.CategoryInfoList import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList @@ -49,7 +48,7 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @Composable fun DiscoverScreen( - showPodcastDetails: (Podcast) -> Unit, + showPodcastDetails: (PodcastInfo) -> Unit, playEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, discoverScreenViewModel: DiscoverScreenViewModel = hiltViewModel() @@ -71,7 +70,7 @@ fun DiscoverScreen( podcastList = s.podcastList, selectedCategory = s.selectedCategory, latestEpisodeList = s.latestEpisodeList, - onPodcastSelected = { showPodcastDetails(it.podcast) }, + onPodcastSelected = showPodcastDetails, onCategorySelected = discoverScreenViewModel::selectCategory, onEpisodeSelected = { discoverScreenViewModel.play(it) @@ -93,7 +92,7 @@ private fun CatalogWithCategorySelection( selectedCategory: CategoryInfo, latestEpisodeList: EpisodeList, - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, onEpisodeSelected: (PlayerEpisode) -> Unit, onCategorySelected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier, diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt index ef766d7e9c..019cb9af43 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt @@ -18,6 +18,7 @@ package com.example.jetcaster.tv.ui.discover import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.jetcaster.core.data.database.model.asExternalModel import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.data.repository.CategoryStore import com.example.jetcaster.core.data.repository.PodcastsRepository @@ -73,8 +74,8 @@ class DiscoverScreenViewModel @Inject constructor( } else { flowOf(emptyList()) } - }.map { - PodcastList(it) + }.map { list -> + PodcastList(list.map { it.asExternalModel() }) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt index 3093bf1856..281c3a8b98 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt @@ -17,11 +17,9 @@ package com.example.jetcaster.tv.ui.episode import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -34,11 +32,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.Episode -import com.example.jetcaster.core.data.database.model.EpisodeToPodcast -import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.model.PlayerEpisode -import com.example.jetcaster.tv.ui.component.Background +import com.example.jetcaster.tv.ui.component.BackgroundContainer import com.example.jetcaster.tv.ui.component.EnqueueButton import com.example.jetcaster.tv.ui.component.EpisodeDataAndDuration import com.example.jetcaster.tv.ui.component.ErrorState @@ -62,7 +57,7 @@ fun EpisodeScreen( EpisodeScreenUiState.Loading -> Loading(modifier = modifier) EpisodeScreenUiState.Error -> ErrorState(backToHome = backToHome, modifier = modifier) is EpisodeScreenUiState.Ready -> EpisodeDetailsWithBackground( - episodeToPodcast = s.episodeToPodcast, + playerEpisode = s.playerEpisode, playEpisode = { episodeScreenViewModel.play(it) playEpisode() @@ -74,15 +69,18 @@ fun EpisodeScreen( @Composable private fun EpisodeDetailsWithBackground( - episodeToPodcast: EpisodeToPodcast, + playerEpisode: PlayerEpisode, playEpisode: (PlayerEpisode) -> Unit, addPlayList: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, ) { - Box(modifier = modifier, contentAlignment = Alignment.Center) { - Background(podcast = episodeToPodcast.podcast, modifier = Modifier.fillMaxSize()) + BackgroundContainer( + playerEpisode = playerEpisode, + contentAlignment = Alignment.Center, + modifier = modifier + ) { EpisodeDetails( - episodeToPodcast = episodeToPodcast, + playerEpisode = playerEpisode, playEpisode = playEpisode, addPlayList = addPlayList, modifier = Modifier @@ -93,7 +91,7 @@ private fun EpisodeDetailsWithBackground( @Composable private fun EpisodeDetails( - episodeToPodcast: EpisodeToPodcast, + playerEpisode: PlayerEpisode, playEpisode: (PlayerEpisode) -> Unit, addPlayList: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, @@ -101,15 +99,15 @@ private fun EpisodeDetails( TwoColumn( first = { Thumbnail( - podcast = episodeToPodcast.podcast, + episode = playerEpisode, size = JetcasterAppDefaults.thumbnailSize.episodeDetails ) }, second = { EpisodeInfo( - episode = episodeToPodcast.episode, - playEpisode = { playEpisode(episodeToPodcast.toPlayerEpisode()) }, - addPlayList = { addPlayList(episodeToPodcast.toPlayerEpisode()) }, + playerEpisode = playerEpisode, + playEpisode = { playEpisode(playerEpisode) }, + addPlayList = { addPlayList(playerEpisode) }, modifier = Modifier.weight(1f) ) }, @@ -120,28 +118,27 @@ private fun EpisodeDetails( @OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun EpisodeInfo( - episode: Episode, + playerEpisode: PlayerEpisode, playEpisode: () -> Unit, addPlayList: () -> Unit, modifier: Modifier = Modifier ) { - val author = episode.author - val duration = episode.duration - val summary = episode.summary + val duration = playerEpisode.duration Column(modifier) { - if (author != null) { - Text(text = author, style = MaterialTheme.typography.bodySmall) - } - Text(text = episode.title, style = MaterialTheme.typography.headlineLarge) + Text(text = playerEpisode.author, style = MaterialTheme.typography.bodySmall) + Text(text = playerEpisode.title, style = MaterialTheme.typography.headlineLarge) if (duration != null) { - EpisodeDataAndDuration(offsetDateTime = episode.published, duration = duration) - } - if (summary != null) { - Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) - Text(text = summary, softWrap = true, maxLines = 5, overflow = TextOverflow.Ellipsis) + EpisodeDataAndDuration(offsetDateTime = playerEpisode.published, duration = duration) } Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) + Text( + text = playerEpisode.summary, + softWrap = true, + maxLines = 5, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) Controls(playEpisode = playEpisode, addPlayList = addPlayList) } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt index 9974d49952..095045d789 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt @@ -19,7 +19,7 @@ package com.example.jetcaster.tv.ui.episode import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.jetcaster.core.data.database.model.EpisodeToPodcast +import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.data.repository.EpisodeStore import com.example.jetcaster.core.data.repository.PodcastsRepository import com.example.jetcaster.core.model.PlayerEpisode @@ -60,7 +60,7 @@ class EpisodeScreenViewModel @Inject constructor( val uiStateFlow = episodeToPodcastFlow.map { if (it != null) { - EpisodeScreenUiState.Ready(it) + EpisodeScreenUiState.Ready(it.toPlayerEpisode()) } else { EpisodeScreenUiState.Error } @@ -88,5 +88,5 @@ class EpisodeScreenViewModel @Inject constructor( sealed interface EpisodeScreenUiState { data object Loading : EpisodeScreenUiState data object Error : EpisodeScreenUiState - data class Ready(val episodeToPodcast: EpisodeToPodcast) : EpisodeScreenUiState + data class Ready(val playerEpisode: PlayerEpisode) : EpisodeScreenUiState } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt index 84cc659c69..1df969607e 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt @@ -36,8 +36,8 @@ import androidx.tv.material3.Button import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.R import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList @@ -49,7 +49,7 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults fun LibraryScreen( modifier: Modifier = Modifier, navigateToDiscover: () -> Unit, - showPodcastDetails: (PodcastWithExtraInfo) -> Unit, + showPodcastDetails: (PodcastInfo) -> Unit, playEpisode: (PlayerEpisode) -> Unit, libraryScreenViewModel: LibraryScreenViewModel = hiltViewModel() ) { @@ -78,7 +78,7 @@ fun LibraryScreen( private fun Library( podcastList: PodcastList, episodeList: EpisodeList, - showPodcastDetails: (PodcastWithExtraInfo) -> Unit, + showPodcastDetails: (PodcastInfo) -> Unit, onEpisodeSelected: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt index 488b5c2da8..2f8769d0bb 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt @@ -18,6 +18,7 @@ package com.example.jetcaster.tv.ui.library import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.jetcaster.core.data.database.model.asExternalModel import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.data.repository.EpisodeStore import com.example.jetcaster.core.data.repository.PodcastStore @@ -45,9 +46,10 @@ class LibraryScreenViewModel @Inject constructor( private val episodePlayer: EpisodePlayer, ) : ViewModel() { - private val followingPodcastListFlow = podcastStore.followedPodcastsSortedByLastEpisode().map { - PodcastList(it) - } + private val followingPodcastListFlow = + podcastStore.followedPodcastsSortedByLastEpisode().map { list -> + PodcastList(list.map { it.asExternalModel() }) + } @OptIn(ExperimentalCoroutinesApi::class) private val latestEpisodeListFlow = podcastStore diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreen.kt index ddc0ce32c5..d99a90e2a9 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreen.kt @@ -58,11 +58,11 @@ import androidx.tv.material3.ButtonDefaults import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.Podcast import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.R import com.example.jetcaster.tv.model.EpisodeList -import com.example.jetcaster.tv.ui.component.Background +import com.example.jetcaster.tv.ui.component.BackgroundContainer import com.example.jetcaster.tv.ui.component.ButtonWithIcon import com.example.jetcaster.tv.ui.component.EnqueueButton import com.example.jetcaster.tv.ui.component.EpisodeDataAndDuration @@ -87,7 +87,7 @@ fun PodcastScreen( PodcastScreenUiState.Loading -> Loading(modifier = modifier) PodcastScreenUiState.Error -> ErrorState(backToHome = backToHomeScreen, modifier = modifier) is PodcastScreenUiState.Ready -> PodcastDetailsWithBackground( - podcast = s.podcast, + podcastInfo = s.podcastInfo, episodeList = s.episodeList, isSubscribed = s.isSubscribed, subscribe = podcastScreenViewModel::subscribe, @@ -104,21 +104,21 @@ fun PodcastScreen( @Composable private fun PodcastDetailsWithBackground( - podcast: Podcast, + podcastInfo: PodcastInfo, episodeList: EpisodeList, isSubscribed: Boolean, - subscribe: (Podcast, Boolean) -> Unit, - unsubscribe: (Podcast, Boolean) -> Unit, + subscribe: (PodcastInfo, Boolean) -> Unit, + unsubscribe: (PodcastInfo, Boolean) -> Unit, playEpisode: (PlayerEpisode) -> Unit, showEpisodeDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() } ) { - Box(modifier = modifier) { - Background(podcast = podcast) + + BackgroundContainer(podcastInfo = podcastInfo, modifier = modifier) { PodcastDetails( - podcast = podcast, + podcastInfo = podcastInfo, episodeList = episodeList, isSubscribed = isSubscribed, subscribe = subscribe, @@ -137,11 +137,11 @@ private fun PodcastDetailsWithBackground( @OptIn(ExperimentalComposeUiApi::class) @Composable private fun PodcastDetails( - podcast: Podcast, + podcastInfo: PodcastInfo, episodeList: EpisodeList, isSubscribed: Boolean, - subscribe: (Podcast, Boolean) -> Unit, - unsubscribe: (Podcast, Boolean) -> Unit, + subscribe: (PodcastInfo, Boolean) -> Unit, + unsubscribe: (PodcastInfo, Boolean) -> Unit, playEpisode: (PlayerEpisode) -> Unit, showEpisodeDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, @@ -154,7 +154,7 @@ private fun PodcastDetails( Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn), first = { PodcastInfo( - podcast = podcast, + podcastInfo = podcastInfo, isSubscribed = isSubscribed, subscribe = subscribe, unsubscribe = unsubscribe, @@ -183,38 +183,32 @@ private fun PodcastDetails( @OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun PodcastInfo( - podcast: Podcast, + podcastInfo: PodcastInfo, isSubscribed: Boolean, - subscribe: (Podcast, Boolean) -> Unit, - unsubscribe: (Podcast, Boolean) -> Unit, + subscribe: (PodcastInfo, Boolean) -> Unit, + unsubscribe: (PodcastInfo, Boolean) -> Unit, modifier: Modifier = Modifier, ) { - val author = podcast.author - val description = podcast.description - Column(modifier = modifier) { - Thumbnail(podcast = podcast) + Thumbnail(podcastInfo = podcastInfo) Spacer(modifier = Modifier.height(16.dp)) - if (author != null) { - Text( - text = author, - style = MaterialTheme.typography.bodySmall - ) - } + Text( - text = podcast.title, + text = podcastInfo.author, + style = MaterialTheme.typography.bodySmall + ) + Text( + text = podcastInfo.title, style = MaterialTheme.typography.headlineSmall, ) - if (description != null) { - Text( - text = description, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium - ) - } + Text( + text = podcastInfo.description, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium + ) ToggleSubscriptionButton( - podcast, + podcastInfo, isSubscribed, subscribe, unsubscribe, @@ -227,10 +221,10 @@ private fun PodcastInfo( @OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun ToggleSubscriptionButton( - podcast: Podcast, + podcastInfo: PodcastInfo, isSubscribed: Boolean, - subscribe: (Podcast, Boolean) -> Unit, - unsubscribe: (Podcast, Boolean) -> Unit, + subscribe: (PodcastInfo, Boolean) -> Unit, + unsubscribe: (PodcastInfo, Boolean) -> Unit, modifier: Modifier = Modifier ) { val icon = if (isSubscribed) { @@ -251,7 +245,7 @@ private fun ToggleSubscriptionButton( ButtonWithIcon( label = label, icon = icon, - onClick = { action(podcast, isSubscribed) }, + onClick = { action(podcastInfo, isSubscribed) }, scale = ButtonDefaults.scale(scale = 1f), modifier = modifier ) diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreenViewModel.kt index ace9275b0c..de478fe10b 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastScreenViewModel.kt @@ -19,11 +19,12 @@ package com.example.jetcaster.tv.ui.podcast import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.jetcaster.core.data.database.model.Podcast +import com.example.jetcaster.core.data.database.model.asExternalModel import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.data.repository.EpisodeStore import com.example.jetcaster.core.data.repository.PodcastStore import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.ui.Screen @@ -48,15 +49,15 @@ class PodcastScreenViewModel @Inject constructor( private val podcastUri = handle.get(Screen.Podcast.PARAMETER_NAME) - private val podcastFlow = if (podcastUri != null) { - podcastStore.podcastWithUri(podcastUri) - } else { - flowOf(null) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - null - ) + @OptIn(ExperimentalCoroutinesApi::class) + private val podcastFlow = + handle.getStateFlow(Screen.Podcast.PARAMETER_NAME, null).flatMapLatest { + if (it != null) { + podcastStore.podcastWithUri(it) + } else { + flowOf(null) + } + } @OptIn(ExperimentalCoroutinesApi::class) private val episodeListFlow = podcastFlow.flatMapLatest { @@ -69,7 +70,8 @@ class PodcastScreenViewModel @Inject constructor( EpisodeList(list.map { it.toPlayerEpisode() }) } - private val subscribedPodcastListFlow = podcastStore.followedPodcastsSortedByLastEpisode() + private val subscribedPodcastListFlow = + podcastStore.followedPodcastsSortedByLastEpisode() val uiStateFlow = combine( podcastFlow, @@ -78,7 +80,7 @@ class PodcastScreenViewModel @Inject constructor( ) { podcast, episodeList, subscribedPodcastList -> if (podcast != null) { val isSubscribed = subscribedPodcastList.any { it.podcast.uri == podcastUri } - PodcastScreenUiState.Ready(podcast, episodeList, isSubscribed) + PodcastScreenUiState.Ready(podcast.asExternalModel(), episodeList, isSubscribed) } else { PodcastScreenUiState.Error } @@ -88,18 +90,18 @@ class PodcastScreenViewModel @Inject constructor( PodcastScreenUiState.Loading ) - fun subscribe(podcast: Podcast, isSubscribed: Boolean) { + fun subscribe(podcastInfo: PodcastInfo, isSubscribed: Boolean) { if (!isSubscribed) { viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcast.uri) + podcastStore.togglePodcastFollowed(podcastInfo.uri) } } } - fun unsubscribe(podcast: Podcast, isSubscribed: Boolean) { + fun unsubscribe(podcastInfo: PodcastInfo, isSubscribed: Boolean) { if (isSubscribed) { viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcast.uri) + podcastStore.togglePodcastFollowed(podcastInfo.uri) } } } @@ -117,7 +119,7 @@ sealed interface PodcastScreenUiState { data object Loading : PodcastScreenUiState data object Error : PodcastScreenUiState data class Ready( - val podcast: Podcast, + val podcastInfo: PodcastInfo, val episodeList: EpisodeList, val isSubscribed: Boolean ) : PodcastScreenUiState diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt index 5fd4e0fecc..a45998749c 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt @@ -55,8 +55,8 @@ import androidx.tv.material3.FilterChip import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.model.CategoryInfo +import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.R import com.example.jetcaster.tv.model.CategorySelectionList import com.example.jetcaster.tv.model.PodcastList @@ -66,7 +66,7 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @Composable fun SearchScreen( - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, searchScreenViewModel: SearchScreenViewModel = hiltViewModel() ) { @@ -124,7 +124,7 @@ private fun HasResult( onKeywordInput: (String) -> Unit, onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, modifier: Modifier = Modifier ) { SearchResult( @@ -255,7 +255,7 @@ private fun CategorySelection( @Composable private fun SearchResult( podcastList: PodcastList, - onPodcastSelected: (PodcastWithExtraInfo) -> Unit, + onPodcastSelected: (PodcastInfo) -> Unit, header: @Composable () -> Unit, modifier: Modifier = Modifier, ) { @@ -270,7 +270,7 @@ private fun SearchResult( header() } items(podcastList) { - PodcastCard(podcast = it.podcast, onClick = { onPodcastSelected(it) }) + PodcastCard(podcastInfo = it, onClick = { onPodcastSelected(it) }) } } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt index 5622aa4d91..cc203f44ec 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt @@ -18,6 +18,7 @@ package com.example.jetcaster.tv.ui.search import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.jetcaster.core.data.database.model.asExternalModel import com.example.jetcaster.core.data.repository.CategoryStore import com.example.jetcaster.core.data.repository.PodcastStore import com.example.jetcaster.core.data.repository.PodcastsRepository @@ -91,7 +92,7 @@ class SearchScreenViewModel @Inject constructor( categorySelectionFlow, searchResultFlow ) { keyword, categorySelection, result -> - val podcastList = PodcastList(result) + val podcastList = PodcastList(result.map { it.asExternalModel() }) when { result.isEmpty() -> SearchScreenUiState.Ready(keyword, categorySelection) else -> SearchScreenUiState.HasResult(keyword, categorySelection, podcastList)