Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
78 changes: 61 additions & 17 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,11 @@ 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.AnimatedPane
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 +98,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 +111,66 @@ import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Home(
navigateToPodcastDetails: (PodcastInfo) -> Unit,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Studio says this parameter is no longer used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in afe8af7

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
AnimatedPane {
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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.rounded.PlayCircleFilled
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -153,7 +153,7 @@ fun EpisodeListItem(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp)
indication = ripple(bounded = false, radius = 24.dp)
) { /* TODO */ }
.size(48.dp)
.padding(6.dp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.shared

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
fun Loading(modifier: Modifier = Modifier) {
Surface(modifier = modifier) {
Box(
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator(
Modifier.align(Alignment.Center)
)
}
}
}
11 changes: 8 additions & 3 deletions Jetcaster/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ androidx-appcompat = "1.6.1"
androidx-benchmark = "1.2.3"
androidx-benchmark-junit4 = "1.2.3"
androidx-compose-bom = "2024.03.00"
androidx-compose-latest = "1.7.0-alpha05"
androidx-compose-material3-adaptive = "1.0.0-alpha09"
androidx-compose-material3-latest = "1.3.0-alpha03"
androidx-constraintlayout = "1.0.1"
androidx-corektx = "1.13.0-beta01"
androidx-glance = "1.0.0"
Expand Down Expand Up @@ -83,13 +86,15 @@ androidx-compose-animation = { module = "androidx.compose.animation:animation" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
androidx-compose-material = { module = "androidx.compose.material:material" }
androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidx-compose-material3-latest" }
androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidx-compose-material3-adaptive" }
androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidx-compose-material3-adaptive" }
androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidx-compose-material3-adaptive" }
androidx-compose-material3-window = { module = "androidx.compose.material3:material3-window-size-class" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidx-compose-latest" }
androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" }
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" }
Expand Down