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 be1b6201f8..92820f8204 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt @@ -42,14 +42,17 @@ import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.example.jetcaster.theme.WearAppTheme import com.example.jetcaster.ui.JetcasterNavController.navigateToLatestEpisode +import com.example.jetcaster.ui.JetcasterNavController.navigateToPodcastDetails import com.example.jetcaster.ui.JetcasterNavController.navigateToUpNext import com.example.jetcaster.ui.JetcasterNavController.navigateToYourPodcast import com.example.jetcaster.ui.LatestEpisodes +import com.example.jetcaster.ui.PodcastDetails import com.example.jetcaster.ui.YourPodcasts 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.player.PlayerScreen +import com.example.jetcaster.ui.podcast.PodcastDetailsScreen import com.google.android.horologist.audio.ui.VolumeViewModel import com.google.android.horologist.media.ui.navigation.MediaNavController.navigateToPlayer import com.google.android.horologist.media.ui.navigation.MediaNavController.navigateToVolume @@ -101,9 +104,8 @@ fun WearApp() { ) { LatestEpisodesScreen( playlistName = stringResource(id = R.string.latest_episodes), - onShuffleButtonClick = { - // navController.navigateToPlayer(it[0].episode.uri) - }, + // TODO implement change speed + onChangeSpeedButtonClick = {}, onPlayButtonClick = { navController.navigateToPlayer() } @@ -111,7 +113,18 @@ fun WearApp() { } composable(route = YourPodcasts.navRoute) { PodcastsScreen( - onPodcastsItemClick = { navController.navigateToPlayer() }, + onPodcastsItemClick = { navController.navigateToPodcastDetails(it.uri) }, + onErrorDialogCancelClick = { navController.popBackStack() } + ) + } + composable(route = PodcastDetails.navRoute) { + PodcastDetailsScreen( + // TODO implement change speed + onChangeSpeedButtonClick = {}, + onPlayButtonClick = { + navController.navigateToPlayer() + }, + onEpisodeItemClick = { navController.navigateToPlayer() }, onErrorDialogCancelClick = { navController.popBackStack() } ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt index c3b9c4d314..cad4aea90f 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt @@ -16,7 +16,11 @@ package com.example.jetcaster.ui +import android.net.Uri +import androidx.navigation.NamedNavArgument import androidx.navigation.NavController +import androidx.navigation.NavType +import androidx.navigation.navArgument import com.google.android.horologist.media.ui.navigation.NavigationScreens /** @@ -32,6 +36,10 @@ public object JetcasterNavController { navigate(LatestEpisodes.destination()) } + public fun NavController.navigateToPodcastDetails(podcastUri: String) { + navigate(PodcastDetails.destination(podcastUri)) + } + public fun NavController.navigateToUpNext() { navigate(UpNext.destination()) } @@ -45,6 +53,21 @@ public object LatestEpisodes : NavigationScreens("latestEpisodes") { public fun destination(): String = navRoute } +public object PodcastDetails : NavigationScreens("podcast?podcastUri={podcastUri}") { + public const val podcastUri: String = "podcastUri" + public fun destination(podcastUriValue: String): String { + val encodedUri = Uri.encode(podcastUriValue) + return "podcast?$podcastUri=$encodedUri" + } + + override val arguments: List + get() = listOf( + navArgument(podcastUri) { + type = NavType.StringType + }, + ) +} + public object UpNext : NavigationScreens("upNext") { public fun destination(): String = navRoute } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt index 58f6f47fce..1cd88a5aa1 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt @@ -52,8 +52,8 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen @Composable fun LatestEpisodesScreen( playlistName: String, - onShuffleButtonClick: (List) -> Unit, - onPlayButtonClick: (List) -> Unit, + onChangeSpeedButtonClick: () -> Unit, + onPlayButtonClick: () -> Unit, modifier: Modifier = Modifier, latestEpisodeViewModel: LatestEpisodeViewModel = hiltViewModel() ) { @@ -62,7 +62,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen modifier = modifier, playlistName = playlistName, viewState = viewState, - onShuffleButtonClick = onShuffleButtonClick, + onChangeSpeedButtonClick = onChangeSpeedButtonClick, onPlayButtonClick = onPlayButtonClick, onPlayEpisode = latestEpisodeViewModel::onPlayEpisode ) @@ -72,8 +72,8 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen fun LatestEpisodeScreen( playlistName: String, viewState: LatestEpisodeViewState, - onShuffleButtonClick: (List) -> Unit, - onPlayButtonClick: (List) -> Unit, + onChangeSpeedButtonClick: () -> Unit, + onPlayButtonClick: () -> Unit, modifier: Modifier = Modifier, onPlayEpisode: (PlayerEpisode) -> Unit, ) { @@ -93,14 +93,16 @@ fun LatestEpisodeScreen( downloadItemArtworkPlaceholder = rememberVectorPainter( image = Icons.Default.MusicNote, tintColor = Color.Blue, - ) + ), + onPlayButtonClick = onPlayButtonClick, + onPlayEpisode = onPlayEpisode ) } }, buttonsContent = { ButtonsContent( viewState = viewState, - onShuffleButtonClick = onShuffleButtonClick, + onChangeSpeedButtonClick = onChangeSpeedButtonClick, onPlayButtonClick = onPlayButtonClick, onPlayEpisode = onPlayEpisode ) @@ -112,7 +114,9 @@ fun LatestEpisodeScreen( @Composable fun MediaContent( episode: EpisodeToPodcast, - downloadItemArtworkPlaceholder: Painter? + downloadItemArtworkPlaceholder: Painter?, + onPlayButtonClick: () -> Unit, + onPlayEpisode: (PlayerEpisode) -> Unit ) { val mediaTitle = episode.episode.title @@ -120,7 +124,10 @@ fun MediaContent( Chip( label = mediaTitle, - onClick = { /*play*/ }, + onClick = { + onPlayButtonClick() + onPlayEpisode(episode.toPlayerEpisode()) + }, secondaryLabel = secondaryLabel, icon = CoilPaintable(episode.podcast.imageUrl, downloadItemArtworkPlaceholder), largeIcon = true, @@ -132,8 +139,8 @@ fun MediaContent( @Composable fun ButtonsContent( viewState: LatestEpisodeViewState, - onShuffleButtonClick: (List) -> Unit, - onPlayButtonClick: (List) -> Unit, + onChangeSpeedButtonClick: () -> Unit, + onPlayButtonClick: () -> Unit, onPlayEpisode: (PlayerEpisode) -> Unit ) { @@ -147,7 +154,7 @@ fun ButtonsContent( Button( imageVector = ImageVector.vectorResource(R.drawable.speed), contentDescription = stringResource(id = R.string.speed_button_content_description), - onClick = { onShuffleButtonClick(viewState.libraryEpisodes) }, + onClick = { onChangeSpeedButtonClick() }, modifier = Modifier .weight(weight = 0.3F, fill = false), ) @@ -156,7 +163,7 @@ fun ButtonsContent( imageVector = Icons.Filled.PlayArrow, contentDescription = stringResource(id = R.string.button_play_content_description), onClick = { - onPlayButtonClick(viewState.libraryEpisodes) + onPlayButtonClick() onPlayEpisode(viewState.libraryEpisodes[0].toPlayerEpisode()) }, modifier = Modifier diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt index 328afba234..5987aa609a 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt @@ -18,15 +18,16 @@ package com.example.jetcaster.ui.library import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.MusicNote import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -44,14 +45,14 @@ import com.example.jetcaster.R import com.example.jetcaster.core.model.PodcastInfo import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.composables.PlaceholderChip -import com.google.android.horologist.composables.Section -import com.google.android.horologist.composables.SectionedList import com.google.android.horologist.compose.layout.ScreenScaffold -import com.google.android.horologist.compose.layout.rememberColumnState +import com.google.android.horologist.compose.layout.rememberResponsiveColumnState import com.google.android.horologist.compose.material.Button import com.google.android.horologist.compose.material.Chip -import com.google.android.horologist.compose.material.Title +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.DefaultEntityScreenHeader +import com.google.android.horologist.media.ui.screens.entity.EntityScreen @Composable fun PodcastsScreen( @@ -122,87 +123,81 @@ fun PodcastsScreen( @ExperimentalHorologistApi @Composable fun PodcastsScreen( - podcastsScreenState: PodcastsScreenState, + podcastsScreenState: PodcastsScreenState, onPodcastsItemClick: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, - podcastItemArtworkPlaceholder: Painter? = null, ) { - val podcastContent: @Composable (podcast: PodcastInfo) -> Unit = { podcast -> - Chip( - label = podcast.title, - onClick = { onPodcastsItemClick(podcast) }, - icon = CoilPaintable(podcast.imageUrl, podcastItemArtworkPlaceholder), - largeIcon = true, - colors = ChipDefaults.secondaryChipColors(), - ) - } - - PodcastsScreen( - podcastsScreenState = podcastsScreenState, - modifier = modifier, - content = { podcast -> - Chip( - label = podcast.title, - onClick = { onPodcastsItemClick(podcast) }, - icon = CoilPaintable(podcast.imageUrl, podcastItemArtworkPlaceholder), - largeIcon = true, - colors = ChipDefaults.secondaryChipColors(), - ) + val columnState = rememberResponsiveColumnState() + ScreenScaffold( + scrollState = columnState, + modifier = modifier + ) { + when (podcastsScreenState) { + is PodcastsScreenState.Loaded -> { + EntityScreen( + columnState = columnState, + headerContent = { + DefaultEntityScreenHeader( + title = stringResource( + R.string.podcasts + ) + ) + }, + content = { + items(count = podcastsScreenState.podcastList.size) { + index -> + MediaContent( + podcast = podcastsScreenState.podcastList[index], + downloadItemArtworkPlaceholder = rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onPodcastsItemClick = onPodcastsItemClick + + ) + } + } + ) + } + PodcastsScreenState.Empty, + PodcastsScreenState.Loading -> { + Column { + PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) + } + } } - ) + } } -@ExperimentalHorologistApi @Composable -fun PodcastsScreen( - podcastsScreenState: PodcastsScreenState, - modifier: Modifier = Modifier, - content: @Composable (podcast: T) -> Unit, +fun MediaContent( + podcast: PodcastInfo, + downloadItemArtworkPlaceholder: Painter?, + onPodcastsItemClick: (PodcastInfo) -> Unit ) { - val columnState = rememberColumnState() - ScreenScaffold(scrollState = columnState) { - SectionedList( - modifier = modifier, - columnState = columnState, - ) { - val sectionState = when (podcastsScreenState) { - is PodcastsScreenState.Loaded -> { - Section.State.Loaded(podcastsScreenState.podcastList) - } - - PodcastsScreenState.Empty -> Section.State.Failed - PodcastsScreenState.Loading -> Section.State.Loading - } - - section(state = sectionState) { - header { - Title( - R.string.podcasts, - Modifier.padding(bottom = 12.dp), - ) - } + val mediaTitle = podcast.title - loaded { content(it) } + val secondaryLabel = podcast.author - loading(count = 4) { - Column { - PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) - } - } - } - } - } + Chip( + label = mediaTitle, + onClick = { onPodcastsItemClick(podcast) }, + secondaryLabel = secondaryLabel, + icon = CoilPaintable(podcast.imageUrl, downloadItemArtworkPlaceholder), + largeIcon = true, + colors = ChipDefaults.secondaryChipColors(), + ) } @ExperimentalHorologistApi -public sealed class PodcastsScreenState { +sealed class PodcastsScreenState { - public object Loading : PodcastsScreenState() + data object Loading : PodcastsScreenState() - public data class Loaded( - val podcastList: List, - ) : PodcastsScreenState() + data class Loaded( + val podcastList: List, + ) : PodcastsScreenState() - public object Empty : PodcastsScreenState() + data object Empty : PodcastsScreenState() } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt index 5e18dc1ebd..1dcca8b9a6 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt @@ -35,7 +35,7 @@ class PodcastsViewModel @Inject constructor( podcastStore: PodcastStore, ) : ViewModel() { - val uiState: StateFlow> = + val uiState: StateFlow = podcastStore.followedPodcastsSortedByLastEpisode(limit = 10).map { if (it.isNotEmpty()) { PodcastsScreenState.Loaded(it.map(PodcastMapper::map)) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt new file mode 100644 index 0000000000..f45f0be1f9 --- /dev/null +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -0,0 +1,211 @@ +/* + * 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.podcast + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.wear.compose.material.ChipDefaults +import com.example.jetcaster.R +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.core.model.PodcastInfo +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.composables.PlaceholderChip +import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults +import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.padding +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.Button +import com.google.android.horologist.compose.material.Chip +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.DefaultEntityScreenHeader +import com.google.android.horologist.media.ui.screens.entity.EntityScreen + +@Composable fun PodcastDetailsScreen( + onChangeSpeedButtonClick: () -> Unit, + onPlayButtonClick: () -> Unit, + onEpisodeItemClick: (EpisodeToPodcast) -> Unit, + onErrorDialogCancelClick: () -> Unit, + modifier: Modifier = Modifier, + podcastDetailsViewModel: PodcastDetailsViewModel = hiltViewModel() +) { + val uiState by podcastDetailsViewModel.uiState.collectAsStateWithLifecycle() + + PodcastDetailsScreen( + viewState = uiState, + onChangeSpeedButtonClick = onChangeSpeedButtonClick, + onEpisodeItemClick = onEpisodeItemClick, + onPlayEpisode = podcastDetailsViewModel::onPlayEpisode, + onErrorDialogCancelClick = onErrorDialogCancelClick, + onPlayButtonClick = onPlayButtonClick, + modifier = modifier, + ) +} + +@Composable +fun PodcastDetailsScreen( + viewState: PodcastDetailsScreenState, + onChangeSpeedButtonClick: () -> Unit, + onPlayButtonClick: () -> Unit, + modifier: Modifier = Modifier, + onEpisodeItemClick: (EpisodeToPodcast) -> Unit, + onPlayEpisode: (PlayerEpisode) -> Unit, + onErrorDialogCancelClick: () -> Unit +) { + val columnState = rememberResponsiveColumnState( + contentPadding = padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip + ) + ) + ScreenScaffold( + scrollState = columnState, + modifier = modifier + ) { + when (viewState) { + is PodcastDetailsScreenState.Loaded -> { + EntityScreen( + columnState = columnState, + headerContent = { DefaultEntityScreenHeader(title = viewState.podcast.title) }, + buttonsContent = { + ButtonsContent( + episodes = viewState.episodeList, + onChangeSpeedButtonClick = onChangeSpeedButtonClick, + onPlayButtonClick = onPlayButtonClick, + onPlayEpisode = onPlayEpisode + ) + }, + content = { + items(count = viewState.episodeList.size) { index -> + MediaContent( + episode = viewState.episodeList[index], + episodeArtworkPlaceholder = rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onEpisodeItemClick + ) + } + } + ) + } + + PodcastDetailsScreenState.Empty, + PodcastDetailsScreenState.Loading -> { + Column { + PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) + } + } + } + } + AlertDialog( + showDialog = viewState == PodcastDetailsScreenState.Empty, + onDismiss = { onErrorDialogCancelClick }, + message = stringResource(R.string.podcasts_no_episode_podcasts) + ) +} + +@OptIn(ExperimentalHorologistApi::class) +@Composable +fun ButtonsContent( + episodes: List, + onChangeSpeedButtonClick: () -> Unit, + onPlayButtonClick: () -> Unit, + onPlayEpisode: (PlayerEpisode) -> Unit +) { + + Row( + modifier = Modifier + .padding(bottom = 16.dp) + .height(52.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), + ) { + Button( + imageVector = ImageVector.vectorResource(R.drawable.speed), + contentDescription = stringResource(id = R.string.speed_button_content_description), + onClick = { onChangeSpeedButtonClick() }, + modifier = Modifier + .weight(weight = 0.3F, fill = false), + ) + + Button( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.button_play_content_description), + onClick = { + onPlayButtonClick() + onPlayEpisode(episodes[0].toPlayerEpisode()) + }, + modifier = Modifier + .weight(weight = 0.3F, fill = false), + ) + } +} + +@Composable +fun MediaContent( + episode: EpisodeToPodcast, + episodeArtworkPlaceholder: Painter?, + onEpisodeItemClick: (EpisodeToPodcast) -> Unit +) { + val mediaTitle = episode.episode.title + + val secondaryLabel = episode.episode.author + + Chip( + label = mediaTitle, + onClick = { onEpisodeItemClick }, + secondaryLabel = secondaryLabel, + icon = CoilPaintable(episode.podcast.imageUrl, episodeArtworkPlaceholder), + largeIcon = true, + colors = ChipDefaults.secondaryChipColors(), + ) +} + +@ExperimentalHorologistApi +sealed class PodcastDetailsScreenState { + + data object Loading : PodcastDetailsScreenState() + + data class Loaded( + val episodeList: List, + val podcast: PodcastInfo, + ) : PodcastDetailsScreenState() + + data object Empty : PodcastDetailsScreenState() +} diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt new file mode 100644 index 0000000000..3de9f6cf6f --- /dev/null +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt @@ -0,0 +1,102 @@ +/* + * 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.podcast + +/* + * 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 android.net.Uri +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.jetcaster.core.data.database.model.asExternalModel +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.player.EpisodePlayer +import com.example.jetcaster.ui.PodcastDetails +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn + +/** + * ViewModel that handles the business logic and screen state of the Podcast details screen. + */ +@HiltViewModel +class PodcastDetailsViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + episodeStore: EpisodeStore, + private val episodePlayer: EpisodePlayer, + podcastStore: PodcastStore +) : ViewModel() { + + private val podcastUri: String = Uri.decode( + savedStateHandle.get(PodcastDetails.podcastUri) + ) + + private val podcastFlow = if (podcastUri != null) { + podcastStore.podcastWithExtraInfo(podcastUri) + } else { + flowOf(null) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null + ) + + val uiState: StateFlow = + combine( + podcastFlow, + episodeStore.episodesInPodcast(podcastUri) + ) { podcast, episodeToPodcasts -> + if (podcast != null) { + PodcastDetailsScreenState.Loaded( + podcast = podcast.podcast.asExternalModel() + .copy(isSubscribed = podcast.isFollowed), + episodeList = episodeToPodcasts, + ) + } else { + PodcastDetailsScreenState.Empty + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + PodcastDetailsScreenState.Loading, + ) + + fun onPlayEpisode(episode: PlayerEpisode) { + episodePlayer.currentEpisode = episode + episodePlayer.play() + } +} diff --git a/Jetcaster/wear/src/main/res/values/strings.xml b/Jetcaster/wear/src/main/res/values/strings.xml index ec7f044650..2b06b5d941 100644 --- a/Jetcaster/wear/src/main/res/values/strings.xml +++ b/Jetcaster/wear/src/main/res/values/strings.xml @@ -62,6 +62,7 @@ Nothing playing No podcasts available at the moment + No episodes available at the moment No title Cancel