From 49dcaad36b4a1bcd55084907f67de0953db156ce Mon Sep 17 00:00:00 2001 From: yongsuk44 Date: Fri, 19 Apr 2024 15:24:16 +0900 Subject: [PATCH 01/13] [Jetcaster] Add tests for category filter limit --- .../domain/PodcastCategoryFilterUseCaseTest.kt | 18 ++++++++++++++++++ .../core/data/repository/TestCategoryStore.kt | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/domain/PodcastCategoryFilterUseCaseTest.kt b/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/domain/PodcastCategoryFilterUseCaseTest.kt index 2f2d5a3b5b..9c4d3b84f6 100644 --- a/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/domain/PodcastCategoryFilterUseCaseTest.kt +++ b/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/domain/PodcastCategoryFilterUseCaseTest.kt @@ -113,6 +113,24 @@ class PodcastCategoryFilterUseCaseTest { result.episodes ) } + + @Test + fun whenCategoryInfoNotNull_verifyLimitFlow() = runTest { + val resultFlow = useCase(testCategory.asExternalModel()) + + categoriesStore.setEpisodesFromPodcast( + testCategory.id, + List(8) { testEpisodeToPodcast }.flatten() + ) + categoriesStore.setPodcastsInCategory( + testCategory.id, + List(4) { testPodcasts }.flatten() + ) + + val result = resultFlow.first() + assertEquals(20, result.episodes.size) + assertEquals(10, result.topPodcasts.size) + } } val testPodcasts = listOf( diff --git a/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/repository/TestCategoryStore.kt b/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/repository/TestCategoryStore.kt index 9b867f0f9e..56ceff9642 100644 --- a/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/repository/TestCategoryStore.kt +++ b/Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/repository/TestCategoryStore.kt @@ -42,14 +42,14 @@ class TestCategoryStore : CategoryStore { categoryId: Long, limit: Int ): Flow> = podcastsInCategoryFlow.map { - it[categoryId] ?: emptyList() + it[categoryId]?.take(limit) ?: emptyList() } override fun episodesFromPodcastsInCategory( categoryId: Long, limit: Int ): Flow> = episodesFromPodcasts.map { - it[categoryId] ?: emptyList() + it[categoryId]?.take(limit) ?: emptyList() } override suspend fun addCategory(category: Category): Long = -1 From fd94c7bec36abae4cd357a46f96781bb0400468d Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Thu, 25 Apr 2024 15:17:10 +0900 Subject: [PATCH 02/13] Switch from AsyncImage to PodcastImage and remove coil from the dependencies. --- .../designsystem/component/PodcastImage.kt | 21 +++++- .../component/thumbnailPlaceholder.kt | 31 ++++++-- Jetcaster/tv-app/build.gradle.kts | 8 +- .../example/jetcaster/tv/model/PodcastList.kt | 6 +- .../example/jetcaster/tv/ui/JetcasterApp.kt | 4 +- .../jetcaster/tv/ui/component/Background.kt | 53 +++++++------ .../jetcaster/tv/ui/component/Catalog.kt | 10 +-- .../jetcaster/tv/ui/component/EpisodeCard.kt | 9 +-- .../jetcaster/tv/ui/component/PodcastCard.kt | 16 ++-- .../jetcaster/tv/ui/component/Thumbnail.kt | 19 +++-- .../tv/ui/discover/DiscoverScreen.kt | 9 +-- .../tv/ui/discover/DiscoverScreenViewModel.kt | 5 +- .../jetcaster/tv/ui/episode/EpisodeScreen.kt | 55 +++++++------- .../tv/ui/episode/EpisodeScreenViewModel.kt | 6 +- .../jetcaster/tv/ui/library/LibraryScreen.kt | 6 +- .../tv/ui/library/LibraryScreenViewModel.kt | 8 +- .../jetcaster/tv/ui/podcast/PodcastScreen.kt | 74 +++++++++---------- .../tv/ui/podcast/PodcastScreenViewModel.kt | 35 +++++---- .../jetcaster/tv/ui/search/SearchScreen.kt | 10 +-- .../tv/ui/search/SearchScreenViewModel.kt | 3 +- 20 files changed, 210 insertions(+), 178 deletions(-) rename Jetcaster/{tv-app/src/main/java/com/example/jetcaster/tv/ui => designsystem/src/main/java/com/example/jetcaster/designsystem}/component/thumbnailPlaceholder.kt (53%) 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..18e4841bc6 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 @@ -18,11 +18,11 @@ package com.example.jetcaster.designsystem.component import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme 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,12 +30,16 @@ 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.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.compose.AsyncImagePainter import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest +import com.example.jetcaster.designsystem.theme.surfaceContainerDark +import com.example.jetcaster.designsystem.theme.surfaceContainerLight @Composable fun PodcastImage( @@ -43,6 +47,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 +78,9 @@ fun PodcastImage( else -> { Box( modifier = Modifier + .background(placeholderBrush) .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerHigh) + ) } } @@ -87,3 +93,14 @@ fun PodcastImage( ) } } + +@Composable +private fun podcastImageBackgroundColor( + isInDarkMode: Boolean = isSystemInDarkTheme() +): Color { + return if (isInDarkMode) { + surfaceContainerDark + } else { + surfaceContainerLight + } +} 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..5cb18e5e7f 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,38 @@ * 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) +fun thumbnailPlaceholder( + brush: Brush = thumbnailPlaceholderDefaultBrush() ): BrushPainter { return BrushPainter(brush) } + +@Composable +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..02f0f71174 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 @@ -20,10 +20,12 @@ 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 +50,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 +71,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 +81,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 +91,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 +120,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) From 48c8bb10dfad9bf9f5ec1ecc11c834d340485950 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Thu, 25 Apr 2024 15:19:23 +0900 Subject: [PATCH 03/13] Switch from AsyncImage to PodcastImage and remove coil from the dependencies. --- .../designsystem/component/PodcastImage.kt | 15 --------------- .../component/thumbnailPlaceholder.kt | 8 -------- .../tv/ui/podcast/PodcastScreenViewModel.kt | 3 +-- 3 files changed, 1 insertion(+), 25 deletions(-) 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 18e4841bc6..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 @@ -18,7 +18,6 @@ package com.example.jetcaster.designsystem.component import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -31,15 +30,12 @@ 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.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.compose.AsyncImagePainter import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest -import com.example.jetcaster.designsystem.theme.surfaceContainerDark -import com.example.jetcaster.designsystem.theme.surfaceContainerLight @Composable fun PodcastImage( @@ -93,14 +89,3 @@ fun PodcastImage( ) } } - -@Composable -private fun podcastImageBackgroundColor( - isInDarkMode: Boolean = isSystemInDarkTheme() -): Color { - return if (isInDarkMode) { - surfaceContainerDark - } else { - surfaceContainerLight - } -} diff --git a/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt index 5cb18e5e7f..865dac3130 100644 --- a/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt +++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt @@ -21,17 +21,9 @@ 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 com.example.jetcaster.designsystem.theme.surfaceVariantDark import com.example.jetcaster.designsystem.theme.surfaceVariantLight -@Composable -fun thumbnailPlaceholder( - brush: Brush = thumbnailPlaceholderDefaultBrush() -): BrushPainter { - return BrushPainter(brush) -} - @Composable internal fun thumbnailPlaceholderDefaultBrush( color: Color = thumbnailPlaceHolderDefaultColor() 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 02f0f71174..40cb8fbde8 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,7 +19,6 @@ 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 @@ -30,7 +29,6 @@ import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.ui.Screen import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -39,6 +37,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class PodcastScreenViewModel @Inject constructor( From 1793321d751d67c9e1f04a7454f5c42abba3c740 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Thu, 25 Apr 2024 15:29:34 +0900 Subject: [PATCH 04/13] spotlessApply --- .../example/jetcaster/tv/ui/podcast/PodcastScreenViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 40cb8fbde8..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 @@ -29,6 +29,7 @@ import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.ui.Screen import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -37,7 +38,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class PodcastScreenViewModel @Inject constructor( From f6fc63fb2d9d7eacd68c50d9c66670c69e58ae37 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Wed, 24 Apr 2024 22:41:51 +0100 Subject: [PATCH 05/13] Refactor home to library --- .../java/com/example/jetcaster/WearApp.kt | 12 +- .../LatestEpisodeViewModel.kt | 2 +- .../LatestEpisodesScreen.kt | 2 +- .../library => podcasts}/PodcastsScreen.kt | 2 +- .../library => podcasts}/PodcastsViewModel.kt | 2 +- .../{ui/library => queue}/QueueScreen.kt | 2 +- .../{ui/library => queue}/QueueViewModel.kt | 2 +- .../jetcaster/ui/home/HomeViewModel.kt | 144 ------------- .../LibraryScreen.kt} | 201 +++++++++++------- .../jetcaster/ui/library/LibraryViewModel.kt | 143 +++++++++++++ 10 files changed, 282 insertions(+), 230 deletions(-) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => latest_episodes}/LatestEpisodeViewModel.kt (98%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => latest_episodes}/LatestEpisodesScreen.kt (99%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => podcasts}/PodcastsScreen.kt (99%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => podcasts}/PodcastsViewModel.kt (98%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => queue}/QueueScreen.kt (99%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => queue}/QueueViewModel.kt (98%) delete mode 100644 Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt rename Jetcaster/wear/src/main/java/com/example/jetcaster/ui/{home/HomeScreen.kt => library/LibraryScreen.kt} (66%) create mode 100644 Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt index ab2933b4ed..1fb7ffe8cc 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt @@ -24,6 +24,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState +import com.example.jetcaster.latest_episodes.LatestEpisodesScreen +import com.example.jetcaster.podcasts.PodcastsScreen +import com.example.jetcaster.queue.QueueScreen import com.example.jetcaster.theme.WearAppTheme import com.example.jetcaster.ui.Episode import com.example.jetcaster.ui.JetcasterNavController.navigateToEpisode @@ -38,10 +41,7 @@ import com.example.jetcaster.ui.PodcastDetails import com.example.jetcaster.ui.UpNext import com.example.jetcaster.ui.YourPodcasts import com.example.jetcaster.ui.episode.EpisodeScreen -import com.example.jetcaster.ui.home.HomeScreen -import com.example.jetcaster.ui.library.LatestEpisodesScreen -import com.example.jetcaster.ui.library.PodcastsScreen -import com.example.jetcaster.ui.library.QueueScreen +import com.example.jetcaster.ui.library.LibraryScreen import com.example.jetcaster.ui.player.PlaybackSpeedScreen import com.example.jetcaster.ui.player.PlayerScreen import com.example.jetcaster.ui.podcast.PodcastDetailsScreen @@ -78,10 +78,10 @@ fun WearApp() { ) }, libraryScreen = { - HomeScreen( + LibraryScreen( onLatestEpisodeClick = { navController.navigateToLatestEpisode() }, onYourPodcastClick = { navController.navigateToYourPodcast() }, - onUpNextClick = { navController.navigateToUpNext() } + onUpNextClick = { navController.navigateToUpNext() }, ) }, categoryEntityScreen = { _, _ -> }, diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodeViewModel.kt similarity index 98% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodeViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodeViewModel.kt index 61cf8e245d..f1de0b5fa2 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodeViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodeViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.latest_episodes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodesScreen.kt similarity index 99% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodesScreen.kt index 55d61233cf..4d5297c9e9 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodesScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.latest_episodes import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsScreen.kt similarity index 99% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsScreen.kt index 635caa7d4f..3663940a56 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.podcasts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsViewModel.kt similarity index 98% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsViewModel.kt index bbe6607a8e..9b548d0a43 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.podcasts import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueScreen.kt similarity index 99% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueScreen.kt index adb8f095a5..22b844e081 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.queue import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueViewModel.kt similarity index 98% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueViewModel.kt index 12d1ab0661..099d9e5ee5 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.queue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt deleted file mode 100644 index a94ceebcb2..0000000000 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.jetcaster.ui.home - -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.Podcast -import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo -import com.example.jetcaster.core.data.database.model.asExternalModel -import com.example.jetcaster.core.data.domain.FilterableCategoriesUseCase -import com.example.jetcaster.core.data.domain.PodcastCategoryFilterUseCase -import com.example.jetcaster.core.data.repository.CategoryStore -import com.example.jetcaster.core.data.repository.EpisodeStore -import com.example.jetcaster.core.data.repository.PodcastStore -import com.example.jetcaster.core.data.repository.PodcastsRepository -import com.example.jetcaster.core.model.CategoryTechnology -import com.example.jetcaster.core.model.FilterableCategoriesModel -import com.example.jetcaster.core.model.PlayerEpisode -import com.example.jetcaster.core.model.PodcastCategoryFilterResult -import com.example.jetcaster.core.player.EpisodePlayer -import com.example.jetcaster.core.util.combine -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -@OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class HomeViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val podcastStore: PodcastStore, - private val episodeStore: EpisodeStore, - private val categoryStore: CategoryStore, - private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase, - private val filterableCategoriesUseCase: FilterableCategoriesUseCase, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { - // Holds our currently selected podcast in the library - private val selectedLibraryPodcast = MutableStateFlow(null) - // Holds our currently selected home category - private val selectedHomeCategory = MutableStateFlow(HomeCategory.Library) - // Holds our currently selected category - private val defaultCategory = categoryStore.getCategory(CategoryTechnology) - - // Holds the view state if the UI is refreshing for new data - private val refreshing = MutableStateFlow(false) - - // Combines the latest value from each of the flows, allowing us to generate a - // view state instance which only contains the latest values. - val uiState = combine( - selectedHomeCategory, - podcastStore.followedPodcastsSortedByLastEpisode(limit = 10), - refreshing, - defaultCategory.flatMapLatest { - filterableCategoriesUseCase(it?.asExternalModel()) - }, - defaultCategory.flatMapLatest { - podcastCategoryFilterUseCase(it?.asExternalModel()) - }, - selectedLibraryPodcast.flatMapLatest { - episodeStore.episodesInPodcast( - podcastUri = it?.uri ?: "", - limit = 20 - ) - }, - episodePlayer.playerState.map { - it.queue - } - ) { - homeCategory, - podcasts, - refreshing, - filterableCategories, - podcastCategoryFilterResult, - libraryEpisodes, - queue -> - selectedHomeCategory.value = homeCategory - - HomeViewState( - selectedHomeCategory = homeCategory, - featuredPodcasts = podcasts.toPersistentList(), - refreshing = refreshing, - filterableCategoriesModel = filterableCategories, - podcastCategoryFilterResult = podcastCategoryFilterResult, - libraryEpisodes = libraryEpisodes, - queue = queue, - errorMessage = null, /* TODO */ - ) - }.stateIn(viewModelScope, SharingStarted.Lazily, initialValue = HomeViewState()) - - init { - refresh(force = false) - } - - private fun refresh(force: Boolean) { - viewModelScope.launch { - refreshing.value = true - podcastsRepository.updatePodcasts(force) - refreshing.value = false - } - } - - fun onTogglePodcastFollowed(podcastUri: String) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcastUri) - } - } -} - -enum class HomeCategory { - Library, -} - -data class HomeViewState( - val featuredPodcasts: List = listOf(), - val refreshing: Boolean = false, - val selectedHomeCategory: HomeCategory = HomeCategory.Library, - val filterableCategoriesModel: FilterableCategoriesModel = FilterableCategoriesModel(), - val podcastCategoryFilterResult: PodcastCategoryFilterResult = PodcastCategoryFilterResult(), - val libraryEpisodes: List = emptyList(), - val queue: List = emptyList(), - val errorMessage: String? = null -) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt similarity index 66% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt index cb4fd40d45..0f0a245888 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt @@ -14,15 +14,14 @@ * limitations under the License. */ -package com.example.jetcaster.ui.home +package com.example.jetcaster.ui.library import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MusicNote import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -31,65 +30,164 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Text import com.example.jetcaster.R +import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.core.model.PodcastInfo import com.google.android.horologist.composables.PlaceholderChip import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.listTextPadding +import com.google.android.horologist.compose.layout.ScalingLazyColumnState import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.layout.rememberResponsiveColumnState -import com.google.android.horologist.compose.material.AlertDialog import com.google.android.horologist.compose.material.Chip +import com.google.android.horologist.compose.material.ListHeaderDefaults import com.google.android.horologist.compose.material.ResponsiveListHeader import com.google.android.horologist.images.base.paintable.DrawableResPaintable import com.google.android.horologist.images.base.util.rememberVectorPainter import com.google.android.horologist.images.coil.CoilPaintable +import com.google.android.horologist.media.ui.screens.entity.EntityScreen @Composable -fun HomeScreen( +fun LibraryScreen( onLatestEpisodeClick: () -> Unit, onYourPodcastClick: () -> Unit, onUpNextClick: () -> Unit, modifier: Modifier = Modifier, - homeViewModel: HomeViewModel = hiltViewModel(), + libraryScreenViewModel: LibraryViewModel = hiltViewModel() ) { - val viewState by homeViewModel.uiState.collectAsStateWithLifecycle() + val uiState by libraryScreenViewModel.uiState.collectAsState() - HomeScreen( - modifier = modifier, - viewState = viewState, - onLatestEpisodeClick = onLatestEpisodeClick, - onYourPodcastClick = onYourPodcastClick, - onUpNextClick = onUpNextClick, - onTogglePodcastFollowed = { - homeViewModel.onTogglePodcastFollowed(it.uri) + val columnState = rememberResponsiveColumnState( + contentPadding = ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), + ) + + when (val s = uiState) { + is LibraryScreenUiState.Loading -> + LoadingScreen( + columnState = columnState, + modifier = modifier + ) + is LibraryScreenUiState.NoSubscribedPodcast -> + NoSubscribedPodcastScreen( + columnState = columnState, + modifier = modifier, + topPodcasts = s.topPodcasts, + onTogglePodcastFollowed = libraryScreenViewModel::onTogglePodcastFollowed + ) + + is LibraryScreenUiState.Ready -> + LibraryScreen( + columnState = columnState, + modifier = modifier, + onLatestEpisodeClick = onLatestEpisodeClick, + onYourPodcastClick = onYourPodcastClick, + onUpNextClick = onUpNextClick, + queue = s.queue + ) + } +} + +@Composable +fun LoadingScreen( + columnState: ScalingLazyColumnState, + modifier: Modifier, +) { + EntityScreen( + columnState = columnState, + headerContent = { + ResponsiveListHeader( + contentPadding = ListHeaderDefaults.firstItemPadding() + ) { + Text(text = stringResource(R.string.loading)) + } }, + modifier = modifier, + content = { + items(count = 2) { + PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) + } + } ) } @Composable -fun HomeScreen( - viewState: HomeViewState, - onLatestEpisodeClick: () -> Unit, - onYourPodcastClick: () -> Unit, - onUpNextClick: () -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit, +fun NoSubscribedPodcastScreen( + columnState: ScalingLazyColumnState, + modifier: Modifier, + topPodcasts: List, + onTogglePodcastFollowed: (uri: String) -> Unit +) { + ScreenScaffold(scrollState = columnState, modifier = modifier) { + ScalingLazyColumn(columnState = columnState) { + item { + ResponsiveListHeader( + modifier = modifier.listTextPadding(), + contentColor = MaterialTheme.colors.onSurface + ) { + Text(stringResource(R.string.entity_no_featured_podcasts)) + } + } + if (topPodcasts.isNotEmpty()) { + items(topPodcasts.take(3)) { podcast -> + PodcastContent( + podcast = podcast, + downloadItemArtworkPlaceholder = rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onClick = { + onTogglePodcastFollowed(podcast.uri) + }, + ) + } + } else { + item { + PlaceholderChip( + contentDescription = "", + colors = ChipDefaults.secondaryChipColors() + ) + } + } + } + } +} + +@Composable +private fun PodcastContent( + podcast: PodcastInfo, + downloadItemArtworkPlaceholder: Painter?, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip, - ), + val mediaTitle = podcast.title + + Chip( + label = mediaTitle, + onClick = onClick, + modifier = modifier, + icon = CoilPaintable(podcast.imageUrl, downloadItemArtworkPlaceholder), + largeIcon = true, + colors = ChipDefaults.secondaryChipColors(), ) - var haveDismissedDialog by remember { mutableStateOf(false) } +} +@Composable +fun LibraryScreen( + columnState: ScalingLazyColumnState, + modifier: Modifier, + onLatestEpisodeClick: () -> Unit, + onYourPodcastClick: () -> Unit, + onUpNextClick: () -> Unit, + queue: List +) { ScreenScaffold(scrollState = columnState, modifier = modifier) { ScalingLazyColumn(columnState = columnState) { item { @@ -119,7 +217,7 @@ fun HomeScreen( } } item { - if (viewState.queue.isEmpty()) { + if (queue.isEmpty()) { QueueEmpty() } else { Chip( @@ -132,51 +230,6 @@ fun HomeScreen( } } } - AlertDialog( - message = stringResource(R.string.entity_no_featured_podcasts), - showDialog = !haveDismissedDialog && viewState.featuredPodcasts.isEmpty(), - onDismiss = { haveDismissedDialog = true }, - - content = { - if (viewState.podcastCategoryFilterResult.topPodcasts.isNotEmpty()) { - items(viewState.podcastCategoryFilterResult.topPodcasts.take(3)) { podcast -> - PodcastContent( - podcast = podcast, - downloadItemArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), - onClick = { - onTogglePodcastFollowed(podcast) - }, - ) - } - } else { - item { - PlaceholderChip( - contentDescription = "", - colors = ChipDefaults.secondaryChipColors() - ) - } - } - } - ) -} -@Composable -private fun PodcastContent( - podcast: PodcastInfo, - downloadItemArtworkPlaceholder: Painter?, - onClick: () -> Unit -) { - val mediaTitle = podcast.title - - Chip( - label = mediaTitle, - onClick = onClick, - icon = CoilPaintable(podcast.imageUrl, downloadItemArtworkPlaceholder), - largeIcon = true, - colors = ChipDefaults.secondaryChipColors(), - ) } @Composable diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt new file mode 100644 index 0000000000..b4074a711e --- /dev/null +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetcaster.ui.library + +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo +import com.example.jetcaster.core.data.database.model.asExternalModel +import com.example.jetcaster.core.data.database.model.toPlayerEpisode +import com.example.jetcaster.core.data.domain.PodcastCategoryFilterUseCase +import com.example.jetcaster.core.data.repository.CategoryStore +import com.example.jetcaster.core.data.repository.EpisodeStore +import com.example.jetcaster.core.data.repository.PodcastStore +import com.example.jetcaster.core.data.repository.PodcastsRepository +import com.example.jetcaster.core.model.CategoryTechnology +import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.core.player.EpisodePlayer +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +@HiltViewModel +class LibraryViewModel @Inject constructor( + private val podcastsRepository: PodcastsRepository, + private val episodeStore: EpisodeStore, + private val podcastStore: PodcastStore, + private val episodePlayer: EpisodePlayer, + private val categoryStore: CategoryStore, + private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase +) : ViewModel() { + + private val defaultCategory = categoryStore.getCategory(CategoryTechnology) + private val topPodcastsFlow = defaultCategory.flatMapLatest { + podcastCategoryFilterUseCase(it?.asExternalModel()) + } + + private val followingPodcastListFlow = podcastStore.followedPodcastsSortedByLastEpisode() + + private val queue = episodePlayer.playerState.map { + it.queue + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val latestEpisodeListFlow = podcastStore + .followedPodcastsSortedByLastEpisode() + .flatMapLatest { podcastList -> + if (podcastList.isNotEmpty()) { + combine(podcastList.map { episodeStore.episodesInPodcast(it.podcast.uri, 1) }) { + it.map { episodes -> + episodes.first() + } + } + } else { + flowOf(emptyList()) + } + }.map { list -> + (list.map { it.toPlayerEpisode() }) + } + + val uiState = + combine( + topPodcastsFlow, + followingPodcastListFlow, + latestEpisodeListFlow, + queue + ) { topPodcasts, podcastList, episodeList, queue -> + if (podcastList.isEmpty()) { + LibraryScreenUiState.NoSubscribedPodcast(topPodcasts.topPodcasts) + } else { + LibraryScreenUiState.Ready(podcastList, episodeList, queue) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + LibraryScreenUiState.Loading + ) + + init { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } + } + + fun playEpisode(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } + + fun onTogglePodcastFollowed(podcastUri: String) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcastUri) + } + } +} + +sealed interface LibraryScreenUiState { + data object Loading : LibraryScreenUiState + data class NoSubscribedPodcast( + val topPodcasts: List + ) : LibraryScreenUiState + data class Ready( + val subscribedPodcastList: List, + val latestEpisodeList: List, + val queue: List + ) : LibraryScreenUiState +} From 4724bb23acd26105e802d3b39e73d5ca0ebb77bb Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Thu, 25 Apr 2024 11:28:16 -0700 Subject: [PATCH 06/13] Run on Ubuntu latest. --- .github/workflows/Crane.yaml | 2 +- .github/workflows/JetLagged.yaml | 2 +- .github/workflows/JetNews.yaml | 2 +- .github/workflows/Jetchat.yaml | 2 +- .github/workflows/Owl.yaml | 2 +- .github/workflows/Reply.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/Crane.yaml b/.github/workflows/Crane.yaml index 678376076d..c0077bd864 100644 --- a/.github/workflows/Crane.yaml +++ b/.github/workflows/Crane.yaml @@ -25,7 +25,7 @@ jobs: test: needs: build - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: diff --git a/.github/workflows/JetLagged.yaml b/.github/workflows/JetLagged.yaml index d8b59b81bc..768e069273 100644 --- a/.github/workflows/JetLagged.yaml +++ b/.github/workflows/JetLagged.yaml @@ -25,7 +25,7 @@ jobs: test: needs: build - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: diff --git a/.github/workflows/JetNews.yaml b/.github/workflows/JetNews.yaml index 04919bafe7..47361b46b4 100644 --- a/.github/workflows/JetNews.yaml +++ b/.github/workflows/JetNews.yaml @@ -25,7 +25,7 @@ jobs: androidTest: needs: build - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: diff --git a/.github/workflows/Jetchat.yaml b/.github/workflows/Jetchat.yaml index 01be0116cc..e82e776e49 100644 --- a/.github/workflows/Jetchat.yaml +++ b/.github/workflows/Jetchat.yaml @@ -25,7 +25,7 @@ jobs: test: needs: build - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: diff --git a/.github/workflows/Owl.yaml b/.github/workflows/Owl.yaml index 45efae67a5..d84d8b72d3 100644 --- a/.github/workflows/Owl.yaml +++ b/.github/workflows/Owl.yaml @@ -25,7 +25,7 @@ jobs: test: needs: build - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: diff --git a/.github/workflows/Reply.yaml b/.github/workflows/Reply.yaml index 9b09941854..5809b07720 100644 --- a/.github/workflows/Reply.yaml +++ b/.github/workflows/Reply.yaml @@ -25,7 +25,7 @@ jobs: test: needs: build - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: From 3a882adefa08e4f32331f39255a5706c0d06ffba Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Thu, 25 Apr 2024 21:45:06 +0100 Subject: [PATCH 07/13] Adds Wear specific section to README --- Jetcaster/README.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/Jetcaster/README.md b/Jetcaster/README.md index 0f8cef1423..b1b8ce41f6 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -14,8 +14,9 @@ project from Android Studio following the steps ## Screenshots ![readme_cover](https://github.com/android/compose-samples/assets/10263978/a58ab950-71aa-48e0-8bc7-85443a1b4f6b) +## Phone app -## Features +### Features This sample has 3 components: the home screen, the podcast details screen, and the player screen @@ -38,7 +39,7 @@ Some other notable things which are implemented: * Images are all provided from each podcast's RSS feed, and loaded using [Coil][coil] library. -## Architecture +### Architecture The app is built in a Redux-style, where each UI 'screen' has its own [ViewModel][viewmodel], which exposes a single [StateFlow][stateflow] containing the entire view state. Each [ViewModel][viewmodel] is responsible for subscribing to any data streams required for the view, as well as exposing functions which allow the UI to send events. Using the example of the home screen in the [`com.example.jetcaster.ui.home`](app/src/main/java/com/example/jetcaster/ui/home) package: @@ -58,6 +59,36 @@ This pattern is used across the different screens: - __Discover:__ [`com.example.jetcaster.ui.home.discover`](app/src/main/java/com/example/jetcaster/ui/home/discover) - __Podcast Category:__ [`com.example.jetcaster.ui.category`](app/src/main/java/com/example/jetcaster/ui/home/category) +## Wear + +This sample showcases a 2 screens pager which allows to navigate between the Player and the Library. +From the library, users can access Latest episodes from followed podcasts, followed podcasts and queue. +From the podcast, users can access to episode details and add the episode to the queue. +From the Player screen, users can access a volume screen and a playback speed screen. + +The sample implements [Wear UX best practices for media apps][mediappsbestpractices], such as: +- Support rotating side button (RSB) and Bezel for scrollable screens +- Display scrollbar on scrolling +- Display the time on top of the screens + +The sample is built using the [Media Toolkit][[mediatoolkit]] which is an open source +project part of [Horologist][horologist] to ease the development of media apps on Wear OS built on top of Compose for Wear. +It provides ready to use UI screens, such the [EntityScreen][entityscreen] +that is used in this sample to implement many screens such as Podcast, LatestEpisodes and Queue. [Horologist][horologist] also provides +a VolumeScreen that can be reused by media apps to conveniently control volume either by interacting with the rotating side button(RSB)/Bezel or by +using the provided buttons. +For simplicity, this sample uses a mock Player which is reused across form factors, +if you want to see an advanced Media sample built on Compose that uses Exoplayer and plays media content, +refer to the [Media Toolkit sample][mediatoolkitsample]. + +The [official media app guidance for Wear OS][ [wearmediaguidance]] +advices to download content on the watch before listening to preserve power, this feature will be added to this sample in future iterations. You can +refer to the [Media Toolkit sample][mediatoolkitsample] to learn how to implement the media download feature. + +### Architecture +The architecture of the Wear app is similar to the phone app architecture: each UI 'screen' has its +own [ViewModel][viewmodel] which exposes a `StateFlow` for the UI to observe. + ## Data ### Podcast data @@ -114,3 +145,9 @@ limitations under the License. [jdk8desugar]: https://developer.android.com/studio/write/java8-support#library-desugaring [coil]: https://coil-kt.github.io/coil/ [wsc]: https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes#window_size_classes + [mediatoolkit]: https://google.github.io/horologist/media-toolkit/ + [mediatoolkitsample]: https://google.github.io/horologist/media-sample/ + [wearmediaguidance]: https://developer.android.com/media/implement/surfaces/wear-os#play-downloaded-content + [horologist]: https://google.github.io/horologist/ + [entityscreen]: https://github.com/google/horologist/blob/main/media/ui/src/main/java/com/google/android/horologist/media/ui/screens/entity/EntityScreen.kt + [mediappsbestpractices]: https://developer.android.com/design/ui/wear/guides/foundations/media-apps \ No newline at end of file From 2b69e3a461659415c2dd7c8eae3303e5b96bf491 Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Thu, 25 Apr 2024 13:33:53 -0700 Subject: [PATCH 08/13] Use macos-13 for Crane. --- .github/workflows/Crane.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Crane.yaml b/.github/workflows/Crane.yaml index c0077bd864..1e099028c8 100644 --- a/.github/workflows/Crane.yaml +++ b/.github/workflows/Crane.yaml @@ -25,7 +25,7 @@ jobs: test: needs: build - runs-on: ubuntu-latest + runs-on: macos-13 timeout-minutes: 30 strategy: matrix: From 40a355f3fb8b4b6c3eee928322c860f0373dfe41 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Fri, 26 Apr 2024 12:04:08 +0100 Subject: [PATCH 09/13] Update Jetcaster/README.md Co-authored-by: Chris Arriola --- Jetcaster/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jetcaster/README.md b/Jetcaster/README.md index b1b8ce41f6..627e3a07af 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -61,7 +61,7 @@ This pattern is used across the different screens: ## Wear -This sample showcases a 2 screens pager which allows to navigate between the Player and the Library. +This sample showcases a 2-screen pager which allows navigation between the Player and the Library. From the library, users can access Latest episodes from followed podcasts, followed podcasts and queue. From the podcast, users can access to episode details and add the episode to the queue. From the Player screen, users can access a volume screen and a playback speed screen. From de31e131badbef5b67f4bb910663d74bdf9d22fb Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Fri, 26 Apr 2024 12:04:21 +0100 Subject: [PATCH 10/13] Update Jetcaster/README.md Co-authored-by: Chris Arriola --- Jetcaster/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jetcaster/README.md b/Jetcaster/README.md index 627e3a07af..f9d041f353 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -62,7 +62,7 @@ This pattern is used across the different screens: ## Wear This sample showcases a 2-screen pager which allows navigation between the Player and the Library. -From the library, users can access Latest episodes from followed podcasts, followed podcasts and queue. +From the Library, users can access latest episodes from subscribed podcasts, and queue. From the podcast, users can access to episode details and add the episode to the queue. From the Player screen, users can access a volume screen and a playback speed screen. From c3b367fb86591787c1bf4ffc8572010ec79323af Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Fri, 26 Apr 2024 12:04:29 +0100 Subject: [PATCH 11/13] Update Jetcaster/README.md Co-authored-by: Chris Arriola --- Jetcaster/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jetcaster/README.md b/Jetcaster/README.md index f9d041f353..00aaa3904d 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -63,7 +63,7 @@ This pattern is used across the different screens: This sample showcases a 2-screen pager which allows navigation between the Player and the Library. From the Library, users can access latest episodes from subscribed podcasts, and queue. -From the podcast, users can access to episode details and add the episode to the queue. +From the podcast, users can access episode details and add episodes to the queue. From the Player screen, users can access a volume screen and a playback speed screen. The sample implements [Wear UX best practices for media apps][mediappsbestpractices], such as: From 420d2ef7cb47ffd7d41ec0f9908398d4910700a2 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Fri, 26 Apr 2024 12:04:37 +0100 Subject: [PATCH 12/13] Update Jetcaster/README.md Co-authored-by: Chris Arriola --- Jetcaster/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jetcaster/README.md b/Jetcaster/README.md index 00aaa3904d..6911db3f1e 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -72,7 +72,7 @@ The sample implements [Wear UX best practices for media apps][mediappsbestpracti - Display the time on top of the screens The sample is built using the [Media Toolkit][[mediatoolkit]] which is an open source -project part of [Horologist][horologist] to ease the development of media apps on Wear OS built on top of Compose for Wear. +project part of [Horologist][horologist] to ease the development of media apps on Wear OS built on top of Compose for Wear. It provides ready to use UI screens, such the [EntityScreen][entityscreen] that is used in this sample to implement many screens such as Podcast, LatestEpisodes and Queue. [Horologist][horologist] also provides a VolumeScreen that can be reused by media apps to conveniently control volume either by interacting with the rotating side button(RSB)/Bezel or by From 4cbf2165dd394d830e5a838407d53a849cb44a36 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Fri, 26 Apr 2024 12:04:42 +0100 Subject: [PATCH 13/13] Update Jetcaster/README.md Co-authored-by: Chris Arriola --- Jetcaster/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jetcaster/README.md b/Jetcaster/README.md index 6911db3f1e..ee0073e81f 100644 --- a/Jetcaster/README.md +++ b/Jetcaster/README.md @@ -87,7 +87,7 @@ refer to the [Media Toolkit sample][mediatoolkitsample] to learn how to implemen ### Architecture The architecture of the Wear app is similar to the phone app architecture: each UI 'screen' has its -own [ViewModel][viewmodel] which exposes a `StateFlow` for the UI to observe. +own [ViewModel][viewmodel] which exposes a `StateFlow` for the UI to observe. ## Data