From 10f2fec93a2ed27488bde113d49ee6f260e7290a Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Mon, 4 Mar 2024 15:54:30 -0800 Subject: [PATCH 1/2] [Jetcaster] Implement 'Your Library' --- .../example/jetcaster/data/EpisodeStore.kt | 2 +- .../jetcaster/data/room/EpisodesDao.kt | 2 +- .../com/example/jetcaster/ui/home/Home.kt | 12 +++- .../jetcaster/ui/home/HomeViewModel.kt | 23 ++++++- .../ui/home/category/PodcastCategory.kt | 4 ++ .../home/category/PodcastCategoryViewModel.kt | 68 ------------------- .../jetcaster/ui/home/library/Library.kt | 26 +++++++ .../java/com/example/jetcaster/util/Flows.kt | 43 ++++++++++++ 8 files changed, 106 insertions(+), 74 deletions(-) delete mode 100644 Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategoryViewModel.kt create mode 100644 Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/data/EpisodeStore.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/data/EpisodeStore.kt index c9b46532db..d60fa6e7c4 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/data/EpisodeStore.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/data/EpisodeStore.kt @@ -39,7 +39,7 @@ class EpisodeStore( fun episodesInPodcast( podcastUri: String, limit: Int = Integer.MAX_VALUE - ): Flow> { + ): Flow> { return episodesDao.episodesForPodcastUri(podcastUri, limit) } diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/data/room/EpisodesDao.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/data/room/EpisodesDao.kt index 88a076be76..52701d6298 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/data/room/EpisodesDao.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/data/room/EpisodesDao.kt @@ -46,7 +46,7 @@ abstract class EpisodesDao : BaseDao { abstract fun episodesForPodcastUri( podcastUri: String, limit: Int - ): Flow> + ): Flow> @Transaction @Query( diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt index f7a102a8a4..571648e9cc 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -76,10 +76,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.example.jetcaster.R import com.example.jetcaster.data.Category +import com.example.jetcaster.data.EpisodeToPodcast import com.example.jetcaster.data.PodcastWithExtraInfo import com.example.jetcaster.ui.home.category.PodcastCategoryViewState import com.example.jetcaster.ui.home.discover.DiscoverViewState import com.example.jetcaster.ui.home.discover.discoverItems +import com.example.jetcaster.ui.home.library.libraryItems import com.example.jetcaster.ui.theme.JetcasterTheme import com.example.jetcaster.ui.theme.Keyline1 import com.example.jetcaster.ui.theme.MinContrastOfPrimaryVsSurface @@ -108,6 +110,7 @@ fun Home( selectedHomeCategory = viewState.selectedHomeCategory, discoverViewState = viewState.discoverViewState, podcastCategoryViewState = viewState.podcastCategoryViewState, + libraryEpisodes = viewState.libraryEpisodes, onHomeCategorySelected = viewModel::onHomeCategorySelected, onCategorySelected = viewModel::onCategorySelected, onPodcastUnfollowed = viewModel::onPodcastUnfollowed, @@ -173,6 +176,7 @@ fun Home( homeCategories: List, discoverViewState: DiscoverViewState, podcastCategoryViewState: PodcastCategoryViewState, + libraryEpisodes: List, modifier: Modifier = Modifier, onPodcastUnfollowed: (String) -> Unit, onHomeCategorySelected: (HomeCategory) -> Unit, @@ -239,6 +243,7 @@ fun Home( homeCategories = homeCategories, discoverViewState = discoverViewState, podcastCategoryViewState = podcastCategoryViewState, + libraryEpisodes = libraryEpisodes, scrimColor = scrimColor, pagerState = pagerState, onPodcastUnfollowed = onPodcastUnfollowed, @@ -260,6 +265,7 @@ private fun HomeContent( homeCategories: List, discoverViewState: DiscoverViewState, podcastCategoryViewState: PodcastCategoryViewState, + libraryEpisodes: List, scrimColor: Color, pagerState: PagerState, modifier: Modifier = Modifier, @@ -303,7 +309,10 @@ private fun HomeContent( when (selectedHomeCategory) { HomeCategory.Library -> { - // TODO + libraryItems( + episodes = libraryEpisodes, + navigateToPlayer = navigateToPlayer + ) } HomeCategory.Discover -> { @@ -504,6 +513,7 @@ fun PreviewHomeContent() { topPodcasts = PreviewPodcastsWithExtraInfo, episodes = PreviewEpisodeToPodcasts, ), + libraryEpisodes = emptyList(), onCategorySelected = {}, onPodcastUnfollowed = {}, navigateToPlayer = {}, diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt index a4f61f90be..6a0304a623 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.viewModelScope import com.example.jetcaster.Graph import com.example.jetcaster.data.Category import com.example.jetcaster.data.CategoryStore +import com.example.jetcaster.data.EpisodeStore +import com.example.jetcaster.data.EpisodeToPodcast import com.example.jetcaster.data.PodcastStore import com.example.jetcaster.data.PodcastWithExtraInfo import com.example.jetcaster.data.PodcastsRepository @@ -43,7 +45,8 @@ import kotlinx.coroutines.launch class HomeViewModel( private val podcastsRepository: PodcastsRepository = Graph.podcastRepository, private val categoryStore: CategoryStore = Graph.categoryStore, - private val podcastStore: PodcastStore = Graph.podcastStore + private val podcastStore: PodcastStore = Graph.podcastStore, + private val episodeStore: EpisodeStore = Graph.episodeStore ) : ViewModel() { // Holds our currently selected home category private val selectedHomeCategory = MutableStateFlow(HomeCategory.Discover) @@ -56,6 +59,16 @@ class HomeViewModel( // Holds the view state if the UI is refreshing for new data private val refreshing = MutableStateFlow(false) + @OptIn(ExperimentalCoroutinesApi::class) + private val libraryEpisodes = podcastStore.followedPodcastsSortedByLastEpisode() + .flatMapLatest { followedPodcasts -> + combine(followedPodcasts.map { p -> + episodeStore.episodesInPodcast(p.podcast.uri, 5) + }) { allEpisodes -> + allEpisodes.toList().flatten().sortedByDescending { it.episode.published } + } + } + private val discover = combine( categoryStore.categoriesSortedByPodcastCount() .onEach { categories -> @@ -110,13 +123,15 @@ class HomeViewModel( podcastStore.followedPodcastsSortedByLastEpisode(limit = 20), refreshing, discover, - podcastCategory + podcastCategory, + libraryEpisodes ) { homeCategories, selectedHomeCategory, podcasts, refreshing, discoverViewState, - podcastCategoryViewState -> + podcastCategoryViewState, + libraryEpisodes -> HomeViewState( homeCategories = homeCategories, selectedHomeCategory = selectedHomeCategory, @@ -124,6 +139,7 @@ class HomeViewModel( refreshing = refreshing, discoverViewState = discoverViewState, podcastCategoryViewState = podcastCategoryViewState, + libraryEpisodes = libraryEpisodes, errorMessage = null, /* TODO */ ) }.catch { throwable -> @@ -181,5 +197,6 @@ data class HomeViewState( val homeCategories: List = emptyList(), val discoverViewState: DiscoverViewState = DiscoverViewState(), val podcastCategoryViewState: PodcastCategoryViewState = PodcastCategoryViewState(), + val libraryEpisodes: List = emptyList(), val errorMessage: String? = null ) diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt index 5bc4032921..e8f3528724 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt @@ -80,6 +80,10 @@ import com.example.jetcaster.util.ToggleFollowPodcastIconButton import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +data class PodcastCategoryViewState( + val topPodcasts: List = emptyList(), + val episodes: List = emptyList() +) fun LazyListScope.podcastCategory( topPodcasts: List, episodes: List, diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategoryViewModel.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategoryViewModel.kt deleted file mode 100644 index c957e9bc14..0000000000 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategoryViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 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.home.category - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.jetcaster.Graph -import com.example.jetcaster.data.CategoryStore -import com.example.jetcaster.data.EpisodeToPodcast -import com.example.jetcaster.data.PodcastStore -import com.example.jetcaster.data.PodcastWithExtraInfo -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch - -class PodcastCategoryViewModel( - private val categoryId: Long, - private val categoryStore: CategoryStore = Graph.categoryStore, - private val podcastStore: PodcastStore = Graph.podcastStore -) : ViewModel() { - private val _state = MutableStateFlow(PodcastCategoryViewState()) - - val state: StateFlow - get() = _state - - init { - viewModelScope.launch { - val recentPodcastsFlow = categoryStore.podcastsInCategorySortedByPodcastCount( - categoryId, - limit = 10 - ) - - val episodesFlow = categoryStore.episodesFromPodcastsInCategory( - categoryId, - limit = 20 - ) - - // Combine our flows and collect them into the view state StateFlow - combine(recentPodcastsFlow, episodesFlow) { topPodcasts, episodes -> - PodcastCategoryViewState( - topPodcasts = topPodcasts, - episodes = episodes - ) - }.collect { _state.value = it } - } - } -} - -data class PodcastCategoryViewState( - val topPodcasts: List = emptyList(), - val episodes: List = emptyList() -) diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt new file mode 100644 index 0000000000..68c62d69e8 --- /dev/null +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt @@ -0,0 +1,26 @@ +package com.example.jetcaster.ui.home.library + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.ui.Modifier +import com.example.jetcaster.data.EpisodeToPodcast +import com.example.jetcaster.ui.home.category.EpisodeListItem + +fun LazyListScope.libraryItems( + episodes: List, + navigateToPlayer: (String) -> Unit +) { + if (episodes.isEmpty()) { + // TODO: Empty state + return + } + + items(episodes, key = { it.episode.uri }) { item -> + EpisodeListItem( + episode = item.episode, + podcast = item.podcast, + onClick = navigateToPlayer, + modifier = Modifier.fillParentMaxWidth() + ) + } +} diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Flows.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Flows.kt index 5d6b114939..65276b7378 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Flows.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Flows.kt @@ -49,3 +49,46 @@ fun combine( args[5] as T6, ) } + +/** + * Combines seven flows into a single flow by combining their latest values using the provided transform function. + * + * @param flow The first flow. + * @param flow2 The second flow. + * @param flow3 The third flow. + * @param flow4 The fourth flow. + * @param flow5 The fifth flow. + * @param flow6 The sixth flow. + * @param flow7 The seventh flow. + * @param transform The transform function to combine the latest values of the seven flows. + * @return A flow that emits the results of the transform function applied to the latest values of the seven flows. + */ +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R +): Flow = + kotlinx.coroutines.flow.combine( + flow, + flow2, + flow3, + flow4, + flow5, + flow6, + flow7 + ) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + ) + } From f674d9bc995d3a43e9c7b379689e764b235c758a Mon Sep 17 00:00:00 2001 From: arriolac Date: Tue, 5 Mar 2024 00:03:54 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20Apply=20Spotless?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jetcaster/ui/home/HomeViewModel.kt | 8 ++-- .../jetcaster/ui/home/library/Library.kt | 44 +++++++++++++------ 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt index 6a0304a623..d81092738b 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt @@ -62,9 +62,11 @@ class HomeViewModel( @OptIn(ExperimentalCoroutinesApi::class) private val libraryEpisodes = podcastStore.followedPodcastsSortedByLastEpisode() .flatMapLatest { followedPodcasts -> - combine(followedPodcasts.map { p -> - episodeStore.episodesInPodcast(p.podcast.uri, 5) - }) { allEpisodes -> + combine( + followedPodcasts.map { p -> + episodeStore.episodesInPodcast(p.podcast.uri, 5) + } + ) { allEpisodes -> allEpisodes.toList().flatten().sortedByDescending { it.episode.published } } } diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt index 68c62d69e8..8f12f6a591 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt @@ -1,3 +1,19 @@ +/* + * 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.home.library import androidx.compose.foundation.lazy.LazyListScope @@ -7,20 +23,20 @@ import com.example.jetcaster.data.EpisodeToPodcast import com.example.jetcaster.ui.home.category.EpisodeListItem fun LazyListScope.libraryItems( - episodes: List, - navigateToPlayer: (String) -> Unit + episodes: List, + navigateToPlayer: (String) -> Unit ) { - if (episodes.isEmpty()) { - // TODO: Empty state - return - } + if (episodes.isEmpty()) { + // TODO: Empty state + return + } - items(episodes, key = { it.episode.uri }) { item -> - EpisodeListItem( - episode = item.episode, - podcast = item.podcast, - onClick = navigateToPlayer, - modifier = Modifier.fillParentMaxWidth() - ) - } + items(episodes, key = { it.episode.uri }) { item -> + EpisodeListItem( + episode = item.episode, + podcast = item.podcast, + onClick = navigateToPlayer, + modifier = Modifier.fillParentMaxWidth() + ) + } }