Skip to content

[Wear] Adds PodcastDetailsScreen #1336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -101,17 +104,27 @@ 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()
}
)
}
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() }
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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())
}
Expand All @@ -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<NamedNavArgument>
get() = listOf(
navArgument(podcastUri) {
type = NavType.StringType
},
)
}

public object UpNext : NavigationScreens("upNext") {
public fun destination(): String = navRoute
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen

@Composable fun LatestEpisodesScreen(
playlistName: String,
onShuffleButtonClick: (List<EpisodeToPodcast>) -> Unit,
onPlayButtonClick: (List<EpisodeToPodcast>) -> Unit,
onChangeSpeedButtonClick: () -> Unit,
onPlayButtonClick: () -> Unit,
modifier: Modifier = Modifier,
latestEpisodeViewModel: LatestEpisodeViewModel = hiltViewModel()
) {
Expand All @@ -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
)
Expand All @@ -72,8 +72,8 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen
fun LatestEpisodeScreen(
playlistName: String,
viewState: LatestEpisodeViewState,
onShuffleButtonClick: (List<EpisodeToPodcast>) -> Unit,
onPlayButtonClick: (List<EpisodeToPodcast>) -> Unit,
onChangeSpeedButtonClick: () -> Unit,
onPlayButtonClick: () -> Unit,
modifier: Modifier = Modifier,
onPlayEpisode: (PlayerEpisode) -> Unit,
) {
Expand All @@ -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
)
Expand All @@ -112,15 +114,20 @@ fun LatestEpisodeScreen(
@Composable
fun MediaContent(
episode: EpisodeToPodcast,
downloadItemArtworkPlaceholder: Painter?
downloadItemArtworkPlaceholder: Painter?,
onPlayButtonClick: () -> Unit,
onPlayEpisode: (PlayerEpisode) -> Unit
) {
val mediaTitle = episode.episode.title

val secondaryLabel = episode.episode.author

Chip(
label = mediaTitle,
onClick = { /*play*/ },
onClick = {
onPlayButtonClick()
onPlayEpisode(episode.toPlayerEpisode())
},
secondaryLabel = secondaryLabel,
icon = CoilPaintable(episode.podcast.imageUrl, downloadItemArtworkPlaceholder),
largeIcon = true,
Expand All @@ -132,8 +139,8 @@ fun MediaContent(
@Composable
fun ButtonsContent(
viewState: LatestEpisodeViewState,
onShuffleButtonClick: (List<EpisodeToPodcast>) -> Unit,
onPlayButtonClick: (List<EpisodeToPodcast>) -> Unit,
onChangeSpeedButtonClick: () -> Unit,
onPlayButtonClick: () -> Unit,
onPlayEpisode: (PlayerEpisode) -> Unit
) {

Expand All @@ -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),
)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -122,87 +123,81 @@ fun PodcastsScreen(
@ExperimentalHorologistApi
@Composable
fun PodcastsScreen(
podcastsScreenState: PodcastsScreenState<PodcastInfo>,
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 <T> PodcastsScreen(
podcastsScreenState: PodcastsScreenState<T>,
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<T> -> {
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<out T> {
sealed class PodcastsScreenState {

public object Loading : PodcastsScreenState<Nothing>()
data object Loading : PodcastsScreenState()

public data class Loaded<T>(
val podcastList: List<T>,
) : PodcastsScreenState<T>()
data class Loaded(
val podcastList: List<PodcastInfo>,
) : PodcastsScreenState()

public object Empty : PodcastsScreenState<Nothing>()
data object Empty : PodcastsScreenState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PodcastsViewModel @Inject constructor(
podcastStore: PodcastStore,
) : ViewModel() {

val uiState: StateFlow<PodcastsScreenState<PodcastInfo>> =
val uiState: StateFlow<PodcastsScreenState> =
podcastStore.followedPodcastsSortedByLastEpisode(limit = 10).map {
if (it.isNotEmpty()) {
PodcastsScreenState.Loaded(it.map(PodcastMapper::map))
Expand Down
Loading