Skip to content

Commit

Permalink
[Jetcaster]: Podcast screen (#1296)
Browse files Browse the repository at this point in the history
Changes:
* Added podcast details screen. 
* Added external models `PodcastInfo`, `EpisodeInfo` and `CategoryInfo`
so that composables don't depend on database entities (`Podcast`,
`Episode` and `Category) directly.

Note that design is not final yet but will address that in another PR.

<img width="407" alt="image"
src="https://github.com/android/compose-samples/assets/463186/e13e811e-5c30-4eea-bd11-50c8152674e0">
  • Loading branch information
arriolac authored Apr 1, 2024
2 parents e08a026 + 27c8ea6 commit 6058f86
Show file tree
Hide file tree
Showing 29 changed files with 1,193 additions and 369 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@

package com.example.jetcaster.ui

import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.EaseIn
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
Expand All @@ -27,9 +34,14 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.window.layout.DisplayFeature
import com.example.jetcaster.R
import com.example.jetcaster.core.data.di.Graph.episodePlayer
import com.example.jetcaster.core.data.di.Graph.episodeStore
import com.example.jetcaster.core.data.di.Graph.podcastStore
import com.example.jetcaster.ui.home.Home
import com.example.jetcaster.ui.player.PlayerScreen
import com.example.jetcaster.ui.player.PlayerViewModel
import com.example.jetcaster.ui.podcast.PodcastDetailsScreen
import com.example.jetcaster.ui.podcast.PodcastDetailsViewModel

@Composable
fun JetcasterApp(
Expand All @@ -44,8 +56,11 @@ fun JetcasterApp(
) {
composable(Screen.Home.route) { backStackEntry ->
Home(
navigateToPlayer = { episodeUri ->
appState.navigateToPlayer(episodeUri, backStackEntry)
navigateToPodcastDetails = { podcast ->
appState.navigateToPodcastDetails(podcast.uri, backStackEntry)
},
navigateToPlayer = { episode ->
appState.navigateToPlayer(episode.uri, backStackEntry)
}
)
}
Expand All @@ -63,6 +78,46 @@ fun JetcasterApp(
onBackPress = appState::navigateBack
)
}
composable(
route = Screen.PodcastDetails.route,
enterTransition = {
fadeIn(
animationSpec = tween(
300, easing = LinearEasing
)
) + slideIntoContainer(
animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.Start
)
},
exitTransition = {
fadeOut(
animationSpec = tween(
300, easing = LinearEasing
)
) + slideOutOfContainer(
animationSpec = tween(300, easing = EaseOut),
towards = AnimatedContentTransitionScope.SlideDirection.End
)
}
) { backStackEntry ->
val podcastDetailsViewModel: PodcastDetailsViewModel = viewModel(
factory = PodcastDetailsViewModel.provideFactory(
episodeStore = episodeStore,
podcastStore = podcastStore,
episodePlayer = episodePlayer,
owner = backStackEntry,
defaultArgs = backStackEntry.arguments
)
)
PodcastDetailsScreen(
viewModel = podcastDetailsViewModel,
navigateToPlayer = { episodePlayer ->
appState.navigateToPlayer(episodePlayer.uri, backStackEntry)
},
navigateBack = appState::navigateBack
)
}
}
} else {
OfflineDialog { appState.refreshOnline() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,20 @@ import androidx.navigation.compose.rememberNavController
*/
sealed class Screen(val route: String) {
object Home : Screen("home")
object Player : Screen("player/{episodeUri}") {
object Player : Screen("player/{$ARG_EPISODE_URI}") {
fun createRoute(episodeUri: String) = "player/$episodeUri"
}

object PodcastDetails : Screen("podcast/{$ARG_PODCAST_URI}") {

val PODCAST_URI = "podcastUri"
fun createRoute(podcastUri: String) = "podcast/$podcastUri"
}

companion object {
val ARG_PODCAST_URI = "podcastUri"
val ARG_EPISODE_URI = "episodeUri"
}
}

@Composable
Expand Down Expand Up @@ -70,6 +81,13 @@ class JetcasterAppState(
}
}

fun navigateToPodcastDetails(podcastUri: String, from: NavBackStackEntry) {
if (from.lifecycleIsResumed()) {
val encodedUri = Uri.encode(podcastUri)
navController.navigate(Screen.PodcastDetails.createRoute(encodedUri))
}
}

fun navigateBack() {
navController.popBackStack()
}
Expand Down
102 changes: 56 additions & 46 deletions Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.example.jetcaster.R
import com.example.jetcaster.core.data.database.model.Category
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.model.CategoryInfo
import com.example.jetcaster.core.data.model.EpisodeInfo
import com.example.jetcaster.core.data.model.FilterableCategoriesModel
import com.example.jetcaster.core.data.model.LibraryInfo
import com.example.jetcaster.core.data.model.PlayerEpisode
import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
import com.example.jetcaster.core.data.model.PodcastInfo
import com.example.jetcaster.ui.home.discover.discoverItems
import com.example.jetcaster.ui.home.library.libraryItems
import com.example.jetcaster.ui.theme.JetcasterTheme
Expand All @@ -99,11 +100,13 @@ import java.time.Duration
import java.time.LocalDateTime
import java.time.OffsetDateTime
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch

@Composable
fun Home(
navigateToPlayer: (String) -> Unit,
navigateToPodcastDetails: (PodcastInfo) -> Unit,
navigateToPlayer: (EpisodeInfo) -> Unit,
viewModel: HomeViewModel = viewModel()
) {
val viewState by viewModel.state.collectAsStateWithLifecycle()
Expand All @@ -115,14 +118,15 @@ fun Home(
selectedHomeCategory = viewState.selectedHomeCategory,
filterableCategoriesModel = viewState.filterableCategoriesModel,
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
libraryEpisodes = viewState.libraryEpisodes,
library = viewState.library,
onHomeCategorySelected = viewModel::onHomeCategorySelected,
onCategorySelected = viewModel::onCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
navigateToPodcastDetails = navigateToPodcastDetails,
navigateToPlayer = navigateToPlayer,
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
onQueuePodcast = viewModel::onQueuePodcast,
onQueueEpisode = viewModel::onQueueEpisode,
modifier = Modifier.fillMaxSize()
)
}
Expand Down Expand Up @@ -174,21 +178,22 @@ fun HomeAppBar(

@Composable
fun Home(
featuredPodcasts: PersistentList<PodcastWithExtraInfo>,
featuredPodcasts: PersistentList<PodcastInfo>,
isRefreshing: Boolean,
selectedHomeCategory: HomeCategory,
homeCategories: List<HomeCategory>,
filterableCategoriesModel: FilterableCategoriesModel,
podcastCategoryFilterResult: PodcastCategoryFilterResult,
libraryEpisodes: List<EpisodeToPodcast>,
library: LibraryInfo,
modifier: Modifier = Modifier,
onPodcastUnfollowed: (String) -> Unit,
onPodcastUnfollowed: (PodcastInfo) -> Unit,
onHomeCategorySelected: (HomeCategory) -> Unit,
onCategorySelected: (Category) -> Unit,
navigateToPlayer: (String) -> Unit,
onTogglePodcastFollowed: (String) -> Unit,
onLibraryPodcastSelected: (Podcast?) -> Unit,
onQueuePodcast: (EpisodeToPodcast) -> Unit,
onCategorySelected: (CategoryInfo) -> Unit,
navigateToPodcastDetails: (PodcastInfo) -> Unit,
navigateToPlayer: (EpisodeInfo) -> Unit,
onTogglePodcastFollowed: (PodcastInfo) -> Unit,
onLibraryPodcastSelected: (PodcastInfo?) -> Unit,
onQueueEpisode: (PlayerEpisode) -> Unit,
) {
// Effect that changes the home category selection when there are no subscribed podcasts
LaunchedEffect(key1 = featuredPodcasts) {
Expand Down Expand Up @@ -223,20 +228,21 @@ fun Home(
homeCategories = homeCategories,
filterableCategoriesModel = filterableCategoriesModel,
podcastCategoryFilterResult = podcastCategoryFilterResult,
libraryEpisodes = libraryEpisodes,
library = library,
scrimColor = scrimColor,
modifier = Modifier.padding(contentPadding),
onPodcastUnfollowed = onPodcastUnfollowed,
onHomeCategorySelected = onHomeCategorySelected,
onCategorySelected = onCategorySelected,
navigateToPodcastDetails = navigateToPodcastDetails,
navigateToPlayer = navigateToPlayer,
onTogglePodcastFollowed = onTogglePodcastFollowed,
onLibraryPodcastSelected = onLibraryPodcastSelected,
onQueuePodcast = {
onQueueEpisode = {
coroutineScope.launch {
snackbarHostState.showSnackbar(snackBarText)
}
onQueuePodcast(it)
onQueueEpisode(it)
}
)
}
Expand All @@ -245,29 +251,30 @@ fun Home(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun HomeContent(
featuredPodcasts: PersistentList<PodcastWithExtraInfo>,
featuredPodcasts: PersistentList<PodcastInfo>,
isRefreshing: Boolean,
selectedHomeCategory: HomeCategory,
homeCategories: List<HomeCategory>,
filterableCategoriesModel: FilterableCategoriesModel,
podcastCategoryFilterResult: PodcastCategoryFilterResult,
libraryEpisodes: List<EpisodeToPodcast>,
library: LibraryInfo,
scrimColor: Color,
modifier: Modifier = Modifier,
onPodcastUnfollowed: (String) -> Unit,
onPodcastUnfollowed: (PodcastInfo) -> Unit,
onHomeCategorySelected: (HomeCategory) -> Unit,
onCategorySelected: (Category) -> Unit,
navigateToPlayer: (String) -> Unit,
onTogglePodcastFollowed: (String) -> Unit,
onLibraryPodcastSelected: (Podcast?) -> Unit,
onQueuePodcast: (EpisodeToPodcast) -> Unit,
onCategorySelected: (CategoryInfo) -> Unit,
navigateToPodcastDetails: (PodcastInfo) -> Unit,
navigateToPlayer: (EpisodeInfo) -> Unit,
onTogglePodcastFollowed: (PodcastInfo) -> Unit,
onLibraryPodcastSelected: (PodcastInfo?) -> Unit,
onQueueEpisode: (PlayerEpisode) -> Unit,
) {
val pagerState = rememberPagerState { featuredPodcasts.size }
LaunchedEffect(pagerState, featuredPodcasts) {
snapshotFlow { pagerState.currentPage }
.collect {
val podcast = featuredPodcasts.getOrNull(pagerState.currentPage)
onLibraryPodcastSelected(podcast?.podcast)
onLibraryPodcastSelected(podcast)
}
}
LazyColumn(modifier = modifier.fillMaxSize()) {
Expand All @@ -277,6 +284,7 @@ private fun HomeContent(
pagerState = pagerState,
items = featuredPodcasts,
onPodcastUnfollowed = onPodcastUnfollowed,
navigateToPodcastDetails = navigateToPodcastDetails,
modifier = Modifier
.fillMaxWidth()
.verticalGradientScrim(
Expand Down Expand Up @@ -305,20 +313,21 @@ private fun HomeContent(
when (selectedHomeCategory) {
HomeCategory.Library -> {
libraryItems(
episodes = libraryEpisodes,
library = library,
navigateToPlayer = navigateToPlayer,
onQueuePodcast = onQueuePodcast
onQueueEpisode = onQueueEpisode
)
}

HomeCategory.Discover -> {
discoverItems(
filterableCategoriesModel = filterableCategoriesModel,
podcastCategoryFilterResult = podcastCategoryFilterResult,
navigateToPodcastDetails = navigateToPodcastDetails,
navigateToPlayer = navigateToPlayer,
onCategorySelected = onCategorySelected,
onTogglePodcastFollowed = onTogglePodcastFollowed,
onQueuePodcast = onQueuePodcast
onQueueEpisode = onQueueEpisode
)
}
}
Expand All @@ -328,8 +337,9 @@ private fun HomeContent(
@Composable
private fun FollowedPodcastItem(
pagerState: PagerState,
items: PersistentList<PodcastWithExtraInfo>,
onPodcastUnfollowed: (String) -> Unit,
items: PersistentList<PodcastInfo>,
onPodcastUnfollowed: (PodcastInfo) -> Unit,
navigateToPodcastDetails: (PodcastInfo) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Expand All @@ -339,6 +349,7 @@ private fun FollowedPodcastItem(
pagerState = pagerState,
items = items,
onPodcastUnfollowed = onPodcastUnfollowed,
navigateToPodcastDetails = navigateToPodcastDetails,
modifier = Modifier.fillMaxWidth()
)

Expand Down Expand Up @@ -403,11 +414,11 @@ private val FEATURED_PODCAST_IMAGE_HEIGHT_DP = 180.dp
@Composable
fun FollowedPodcasts(
pagerState: PagerState,
items: PersistentList<PodcastWithExtraInfo>,
items: PersistentList<PodcastInfo>,
onPodcastUnfollowed: (PodcastInfo) -> Unit,
navigateToPodcastDetails: (PodcastInfo) -> Unit,
modifier: Modifier = Modifier,
onPodcastUnfollowed: (String) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
// TODO: Using BoxWithConstraints is not quite performant since it requires 2 passes to compute
// the content padding. This should be revisited once a carousel component is available.
// Alternatively, version 1.7.0-alpha05 of Compose Foundation supports `snapPosition`
Expand All @@ -424,18 +435,16 @@ fun FollowedPodcasts(
pageSpacing = 24.dp,
pageSize = PageSize.Fixed(FEATURED_PODCAST_IMAGE_WIDTH_DP)
) { page ->
val (podcast, lastEpisodeDate) = items[page]
val podcast = items[page]
FollowedPodcastCarouselItem(
podcastImageUrl = podcast.imageUrl,
podcastTitle = podcast.title,
onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) },
lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },
onUnfollowedClick = { onPodcastUnfollowed(podcast) },
lastEpisodeDateText = podcast.lastEpisodeDate?.let { lastUpdated(it) },
modifier = Modifier
.fillMaxSize()
.clickable {
coroutineScope.launch {
pagerState.animateScrollToPage(page)
}
navigateToPodcastDetails(podcast)
}
)
}
Expand Down Expand Up @@ -511,7 +520,7 @@ private fun lastUpdated(updated: OffsetDateTime): String {
fun PreviewHomeContent() {
JetcasterTheme {
Home(
featuredPodcasts = PreviewPodcastsWithExtraInfo,
featuredPodcasts = PreviewPodcasts.toPersistentList(),
isRefreshing = false,
homeCategories = HomeCategory.entries,
selectedHomeCategory = HomeCategory.Discover,
Expand All @@ -520,17 +529,18 @@ fun PreviewHomeContent() {
selectedCategory = PreviewCategories.firstOrNull()
),
podcastCategoryFilterResult = PodcastCategoryFilterResult(
topPodcasts = PreviewPodcastsWithExtraInfo,
episodes = PreviewEpisodeToPodcasts,
topPodcasts = PreviewPodcasts,
episodes = PreviewPodcastCategoryEpisodes
),
libraryEpisodes = emptyList(),
library = LibraryInfo(),
onCategorySelected = {},
onPodcastUnfollowed = {},
navigateToPodcastDetails = {},
navigateToPlayer = {},
onHomeCategorySelected = {},
onTogglePodcastFollowed = {},
onLibraryPodcastSelected = {},
onQueuePodcast = {}
onQueueEpisode = {}
)
}
}
Expand Down
Loading

0 comments on commit 6058f86

Please sign in to comment.