Skip to content
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
4 changes: 4 additions & 0 deletions Jetcaster/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ dependencies {
implementation(libs.androidx.constraintlayout.compose)

implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.window)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.tooling.preview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ fun JetcasterApp(
) {
composable(Screen.Home.route) { backStackEntry ->
Home(
navigateToPodcastDetails = { podcast ->
appState.navigateToPodcastDetails(podcast.uri, backStackEntry)
},
navigateToPlayer = { episode ->
appState.navigateToPlayer(episode.uri, backStackEntry)
}
Expand Down
76 changes: 58 additions & 18 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 @@ -18,6 +18,7 @@

package com.example.jetcaster.ui.home

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
Expand Down Expand Up @@ -63,6 +64,10 @@ import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand Down Expand Up @@ -92,6 +97,8 @@ 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.podcast.PodcastDetailsScreen
import com.example.jetcaster.ui.podcast.PodcastDetailsViewModel
import com.example.jetcaster.ui.theme.JetcasterTheme
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
import com.example.jetcaster.util.quantityStringResource
Expand All @@ -103,30 +110,63 @@ import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Home(
navigateToPodcastDetails: (PodcastInfo) -> Unit,
navigateToPlayer: (EpisodeInfo) -> Unit,
viewModel: HomeViewModel = viewModel()
) {
val viewState by viewModel.state.collectAsStateWithLifecycle()
Surface(Modifier.fillMaxSize()) {
Home(
featuredPodcasts = viewState.featuredPodcasts,
isRefreshing = viewState.refreshing,
homeCategories = viewState.homeCategories,
selectedHomeCategory = viewState.selectedHomeCategory,
filterableCategoriesModel = viewState.filterableCategoriesModel,
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
library = viewState.library,
onHomeCategorySelected = viewModel::onHomeCategorySelected,
onCategorySelected = viewModel::onCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
navigateToPodcastDetails = navigateToPodcastDetails,
navigateToPlayer = navigateToPlayer,
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
onQueueEpisode = viewModel::onQueueEpisode,
val navigator = rememberSupportingPaneScaffoldNavigator<String>(
isDestinationHistoryAware = false
)
BackHandler(enabled = navigator.canNavigateBack()) {
navigator.navigateBack()
}
Surface {
SupportingPaneScaffold(
value = navigator.scaffoldValue,
directive = navigator.scaffoldDirective,
supportingPane = {
val podcastUri = navigator.currentDestination?.content
?: viewState.featuredPodcasts.firstOrNull()?.uri
if (!podcastUri.isNullOrEmpty()) {
val podcastDetailsViewModel = PodcastDetailsViewModel(
podcastUri = podcastUri
)
PodcastDetailsScreen(
viewModel = podcastDetailsViewModel,
navigateToPlayer = navigateToPlayer,
navigateBack = {
if (navigator.canNavigateBack()) {
navigator.navigateBack()
}
}
)
}
},
mainPane = {
Home(
featuredPodcasts = viewState.featuredPodcasts,
isRefreshing = viewState.refreshing,
homeCategories = viewState.homeCategories,
selectedHomeCategory = viewState.selectedHomeCategory,
filterableCategoriesModel = viewState.filterableCategoriesModel,
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
library = viewState.library,
onHomeCategorySelected = viewModel::onHomeCategorySelected,
onCategorySelected = viewModel::onCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
navigateToPodcastDetails = {
navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, it.uri)
},
navigateToPlayer = navigateToPlayer,
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
onQueueEpisode = viewModel::onQueueEpisode,
modifier = Modifier.fillMaxSize()
)
},
modifier = Modifier.fillMaxSize()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ fun LazyListScope.libraryItems(
onClick = navigateToPlayer,
onQueueEpisode = onQueueEpisode,
modifier = Modifier.fillParentMaxWidth(),
showDivider = index != 0
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import com.example.jetcaster.designsystem.theme.Keyline1
import com.example.jetcaster.ui.home.PreviewEpisodes
import com.example.jetcaster.ui.home.PreviewPodcasts
import com.example.jetcaster.ui.shared.EpisodeListItem
import com.example.jetcaster.ui.shared.Loading
import kotlinx.coroutines.launch

@Composable
Expand All @@ -79,15 +80,31 @@ fun PodcastDetailsScreen(
modifier: Modifier = Modifier
) {
val state by viewModel.state.collectAsStateWithLifecycle()
PodcastDetailsScreen(
podcast = state.podcast,
episodes = state.episodes,
toggleSubscribe = viewModel::toggleSusbcribe,
onQueueEpisode = viewModel::onQueueEpisode,
navigateToPlayer = navigateToPlayer,
navigateBack = navigateBack,
modifier = modifier,
)
when (val s = state) {
is PodcastUiState.Loading -> {
PodcastDetailsLoadingScreen(
modifier = Modifier.fillMaxSize()
)
}
is PodcastUiState.Ready -> {
PodcastDetailsScreen(
podcast = s.podcast,
episodes = s.episodes,
toggleSubscribe = viewModel::toggleSusbcribe,
onQueueEpisode = viewModel::onQueueEpisode,
navigateToPlayer = navigateToPlayer,
navigateBack = navigateBack,
modifier = modifier,
)
}
}
}

@Composable
private fun PodcastDetailsLoadingScreen(
modifier: Modifier = Modifier
) {
Loading(modifier = modifier)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

data class PodcastUiState(
val podcast: PodcastInfo = PodcastInfo(),
val episodes: List<EpisodeInfo> = emptyList()
)
sealed interface PodcastUiState {
data object Loading : PodcastUiState
data class Ready(
val podcast: PodcastInfo,
val episodes: List<EpisodeInfo>,
) : PodcastUiState
}

/**
* ViewModel that handles the business logic and screen state of the Podcast details screen.
Expand All @@ -50,26 +53,35 @@ class PodcastDetailsViewModel(
private val episodeStore: EpisodeStore = Graph.episodeStore,
private val episodePlayer: EpisodePlayer = Graph.episodePlayer,
private val podcastStore: PodcastStore = Graph.podcastStore,
savedStateHandle: SavedStateHandle
private val podcastUri: String
) : ViewModel() {

private val podcastUri: String =
Uri.decode(savedStateHandle.get<String>(Screen.ARG_PODCAST_URI)!!)
constructor(
episodeStore: EpisodeStore = Graph.episodeStore,
episodePlayer: EpisodePlayer = Graph.episodePlayer,
podcastStore: PodcastStore = Graph.podcastStore,
savedStateHandle: SavedStateHandle
) : this(
episodeStore = episodeStore,
episodePlayer = episodePlayer,
podcastStore = podcastStore,
podcastUri = Uri.decode(savedStateHandle.get<String>(Screen.ARG_PODCAST_URI)!!)
)

val state: StateFlow<PodcastUiState> =
combine(
podcastStore.podcastWithExtraInfo(podcastUri),
episodeStore.episodesInPodcast(podcastUri)
) { podcast, episodeToPodcasts ->
val episodes = episodeToPodcasts.map { it.episode.asExternalModel() }
PodcastUiState(
PodcastUiState.Ready(
podcast = podcast.podcast.asExternalModel().copy(isSubscribed = podcast.isFollowed),
episodes = episodes,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = PodcastUiState()
initialValue = PodcastUiState.Loading
)

fun toggleSusbcribe(podcast: PodcastInfo) {
Expand Down
Loading