From f6fc63fb2d9d7eacd68c50d9c66670c69e58ae37 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Wed, 24 Apr 2024 22:41:51 +0100 Subject: [PATCH] Refactor home to library --- .../java/com/example/jetcaster/WearApp.kt | 12 +- .../LatestEpisodeViewModel.kt | 2 +- .../LatestEpisodesScreen.kt | 2 +- .../library => podcasts}/PodcastsScreen.kt | 2 +- .../library => podcasts}/PodcastsViewModel.kt | 2 +- .../{ui/library => queue}/QueueScreen.kt | 2 +- .../{ui/library => queue}/QueueViewModel.kt | 2 +- .../jetcaster/ui/home/HomeViewModel.kt | 144 ------------- .../LibraryScreen.kt} | 201 +++++++++++------- .../jetcaster/ui/library/LibraryViewModel.kt | 143 +++++++++++++ 10 files changed, 282 insertions(+), 230 deletions(-) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => latest_episodes}/LatestEpisodeViewModel.kt (98%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => latest_episodes}/LatestEpisodesScreen.kt (99%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => podcasts}/PodcastsScreen.kt (99%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => podcasts}/PodcastsViewModel.kt (98%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => queue}/QueueScreen.kt (99%) rename Jetcaster/wear/src/main/java/com/example/jetcaster/{ui/library => queue}/QueueViewModel.kt (98%) delete mode 100644 Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt rename Jetcaster/wear/src/main/java/com/example/jetcaster/ui/{home/HomeScreen.kt => library/LibraryScreen.kt} (66%) create mode 100644 Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt index ab2933b4ed..1fb7ffe8cc 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt @@ -24,6 +24,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState +import com.example.jetcaster.latest_episodes.LatestEpisodesScreen +import com.example.jetcaster.podcasts.PodcastsScreen +import com.example.jetcaster.queue.QueueScreen import com.example.jetcaster.theme.WearAppTheme import com.example.jetcaster.ui.Episode import com.example.jetcaster.ui.JetcasterNavController.navigateToEpisode @@ -38,10 +41,7 @@ import com.example.jetcaster.ui.PodcastDetails import com.example.jetcaster.ui.UpNext import com.example.jetcaster.ui.YourPodcasts import com.example.jetcaster.ui.episode.EpisodeScreen -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.library.QueueScreen +import com.example.jetcaster.ui.library.LibraryScreen import com.example.jetcaster.ui.player.PlaybackSpeedScreen import com.example.jetcaster.ui.player.PlayerScreen import com.example.jetcaster.ui.podcast.PodcastDetailsScreen @@ -78,10 +78,10 @@ fun WearApp() { ) }, libraryScreen = { - HomeScreen( + LibraryScreen( onLatestEpisodeClick = { navController.navigateToLatestEpisode() }, onYourPodcastClick = { navController.navigateToYourPodcast() }, - onUpNextClick = { navController.navigateToUpNext() } + onUpNextClick = { navController.navigateToUpNext() }, ) }, categoryEntityScreen = { _, _ -> }, diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodeViewModel.kt similarity index 98% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodeViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodeViewModel.kt index 61cf8e245d..f1de0b5fa2 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodeViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodeViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.latest_episodes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodesScreen.kt similarity index 99% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodesScreen.kt index 55d61233cf..4d5297c9e9 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LatestEpisodesScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/latest_episodes/LatestEpisodesScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.latest_episodes import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsScreen.kt similarity index 99% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsScreen.kt index 635caa7d4f..3663940a56 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.podcasts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsViewModel.kt similarity index 98% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsViewModel.kt index bbe6607a8e..9b548d0a43 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/PodcastsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/podcasts/PodcastsViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.podcasts import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueScreen.kt similarity index 99% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueScreen.kt index adb8f095a5..22b844e081 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.queue import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueViewModel.kt similarity index 98% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueViewModel.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueViewModel.kt index 12d1ab0661..099d9e5ee5 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/queue/QueueViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jetcaster.ui.library +package com.example.jetcaster.queue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt deleted file mode 100644 index a94ceebcb2..0000000000 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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 - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -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.database.model.asExternalModel -import com.example.jetcaster.core.data.domain.FilterableCategoriesUseCase -import com.example.jetcaster.core.data.domain.PodcastCategoryFilterUseCase -import com.example.jetcaster.core.data.repository.CategoryStore -import com.example.jetcaster.core.data.repository.EpisodeStore -import com.example.jetcaster.core.data.repository.PodcastStore -import com.example.jetcaster.core.data.repository.PodcastsRepository -import com.example.jetcaster.core.model.CategoryTechnology -import com.example.jetcaster.core.model.FilterableCategoriesModel -import com.example.jetcaster.core.model.PlayerEpisode -import com.example.jetcaster.core.model.PodcastCategoryFilterResult -import com.example.jetcaster.core.player.EpisodePlayer -import com.example.jetcaster.core.util.combine -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -@OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class HomeViewModel @Inject constructor( - private val podcastsRepository: PodcastsRepository, - private val podcastStore: PodcastStore, - private val episodeStore: EpisodeStore, - private val categoryStore: CategoryStore, - private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase, - private val filterableCategoriesUseCase: FilterableCategoriesUseCase, - private val episodePlayer: EpisodePlayer, -) : ViewModel() { - // Holds our currently selected podcast in the library - private val selectedLibraryPodcast = MutableStateFlow(null) - // Holds our currently selected home category - private val selectedHomeCategory = MutableStateFlow(HomeCategory.Library) - // Holds our currently selected category - private val defaultCategory = categoryStore.getCategory(CategoryTechnology) - - // Holds the view state if the UI is refreshing for new data - private val refreshing = MutableStateFlow(false) - - // Combines the latest value from each of the flows, allowing us to generate a - // view state instance which only contains the latest values. - val uiState = combine( - selectedHomeCategory, - podcastStore.followedPodcastsSortedByLastEpisode(limit = 10), - refreshing, - defaultCategory.flatMapLatest { - filterableCategoriesUseCase(it?.asExternalModel()) - }, - defaultCategory.flatMapLatest { - podcastCategoryFilterUseCase(it?.asExternalModel()) - }, - selectedLibraryPodcast.flatMapLatest { - episodeStore.episodesInPodcast( - podcastUri = it?.uri ?: "", - limit = 20 - ) - }, - episodePlayer.playerState.map { - it.queue - } - ) { - homeCategory, - podcasts, - refreshing, - filterableCategories, - podcastCategoryFilterResult, - libraryEpisodes, - queue -> - selectedHomeCategory.value = homeCategory - - HomeViewState( - selectedHomeCategory = homeCategory, - featuredPodcasts = podcasts.toPersistentList(), - refreshing = refreshing, - filterableCategoriesModel = filterableCategories, - podcastCategoryFilterResult = podcastCategoryFilterResult, - libraryEpisodes = libraryEpisodes, - queue = queue, - errorMessage = null, /* TODO */ - ) - }.stateIn(viewModelScope, SharingStarted.Lazily, initialValue = HomeViewState()) - - init { - refresh(force = false) - } - - private fun refresh(force: Boolean) { - viewModelScope.launch { - refreshing.value = true - podcastsRepository.updatePodcasts(force) - refreshing.value = false - } - } - - fun onTogglePodcastFollowed(podcastUri: String) { - viewModelScope.launch { - podcastStore.togglePodcastFollowed(podcastUri) - } - } -} - -enum class HomeCategory { - Library, -} - -data class HomeViewState( - val featuredPodcasts: List = listOf(), - val refreshing: Boolean = false, - val selectedHomeCategory: HomeCategory = HomeCategory.Library, - val filterableCategoriesModel: FilterableCategoriesModel = FilterableCategoriesModel(), - val podcastCategoryFilterResult: PodcastCategoryFilterResult = PodcastCategoryFilterResult(), - val libraryEpisodes: List = emptyList(), - val queue: List = emptyList(), - val errorMessage: String? = null -) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt similarity index 66% rename from Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeScreen.kt rename to Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt index cb4fd40d45..0f0a245888 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/home/HomeScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt @@ -14,15 +14,14 @@ * limitations under the License. */ -package com.example.jetcaster.ui.home +package com.example.jetcaster.ui.library import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MusicNote import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -31,65 +30,164 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Text import com.example.jetcaster.R +import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.core.model.PodcastInfo import com.google.android.horologist.composables.PlaceholderChip import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.listTextPadding +import com.google.android.horologist.compose.layout.ScalingLazyColumnState import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.layout.rememberResponsiveColumnState -import com.google.android.horologist.compose.material.AlertDialog import com.google.android.horologist.compose.material.Chip +import com.google.android.horologist.compose.material.ListHeaderDefaults import com.google.android.horologist.compose.material.ResponsiveListHeader import com.google.android.horologist.images.base.paintable.DrawableResPaintable 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.EntityScreen @Composable -fun HomeScreen( +fun LibraryScreen( onLatestEpisodeClick: () -> Unit, onYourPodcastClick: () -> Unit, onUpNextClick: () -> Unit, modifier: Modifier = Modifier, - homeViewModel: HomeViewModel = hiltViewModel(), + libraryScreenViewModel: LibraryViewModel = hiltViewModel() ) { - val viewState by homeViewModel.uiState.collectAsStateWithLifecycle() + val uiState by libraryScreenViewModel.uiState.collectAsState() - HomeScreen( - modifier = modifier, - viewState = viewState, - onLatestEpisodeClick = onLatestEpisodeClick, - onYourPodcastClick = onYourPodcastClick, - onUpNextClick = onUpNextClick, - onTogglePodcastFollowed = { - homeViewModel.onTogglePodcastFollowed(it.uri) + val columnState = rememberResponsiveColumnState( + contentPadding = ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), + ) + + when (val s = uiState) { + is LibraryScreenUiState.Loading -> + LoadingScreen( + columnState = columnState, + modifier = modifier + ) + is LibraryScreenUiState.NoSubscribedPodcast -> + NoSubscribedPodcastScreen( + columnState = columnState, + modifier = modifier, + topPodcasts = s.topPodcasts, + onTogglePodcastFollowed = libraryScreenViewModel::onTogglePodcastFollowed + ) + + is LibraryScreenUiState.Ready -> + LibraryScreen( + columnState = columnState, + modifier = modifier, + onLatestEpisodeClick = onLatestEpisodeClick, + onYourPodcastClick = onYourPodcastClick, + onUpNextClick = onUpNextClick, + queue = s.queue + ) + } +} + +@Composable +fun LoadingScreen( + columnState: ScalingLazyColumnState, + modifier: Modifier, +) { + EntityScreen( + columnState = columnState, + headerContent = { + ResponsiveListHeader( + contentPadding = ListHeaderDefaults.firstItemPadding() + ) { + Text(text = stringResource(R.string.loading)) + } }, + modifier = modifier, + content = { + items(count = 2) { + PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) + } + } ) } @Composable -fun HomeScreen( - viewState: HomeViewState, - onLatestEpisodeClick: () -> Unit, - onYourPodcastClick: () -> Unit, - onUpNextClick: () -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit, +fun NoSubscribedPodcastScreen( + columnState: ScalingLazyColumnState, + modifier: Modifier, + topPodcasts: List, + onTogglePodcastFollowed: (uri: String) -> Unit +) { + ScreenScaffold(scrollState = columnState, modifier = modifier) { + ScalingLazyColumn(columnState = columnState) { + item { + ResponsiveListHeader( + modifier = modifier.listTextPadding(), + contentColor = MaterialTheme.colors.onSurface + ) { + Text(stringResource(R.string.entity_no_featured_podcasts)) + } + } + if (topPodcasts.isNotEmpty()) { + items(topPodcasts.take(3)) { podcast -> + PodcastContent( + podcast = podcast, + downloadItemArtworkPlaceholder = rememberVectorPainter( + image = Icons.Default.MusicNote, + tintColor = Color.Blue, + ), + onClick = { + onTogglePodcastFollowed(podcast.uri) + }, + ) + } + } else { + item { + PlaceholderChip( + contentDescription = "", + colors = ChipDefaults.secondaryChipColors() + ) + } + } + } + } +} + +@Composable +private fun PodcastContent( + podcast: PodcastInfo, + downloadItemArtworkPlaceholder: Painter?, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip, - ), + val mediaTitle = podcast.title + + Chip( + label = mediaTitle, + onClick = onClick, + modifier = modifier, + icon = CoilPaintable(podcast.imageUrl, downloadItemArtworkPlaceholder), + largeIcon = true, + colors = ChipDefaults.secondaryChipColors(), ) - var haveDismissedDialog by remember { mutableStateOf(false) } +} +@Composable +fun LibraryScreen( + columnState: ScalingLazyColumnState, + modifier: Modifier, + onLatestEpisodeClick: () -> Unit, + onYourPodcastClick: () -> Unit, + onUpNextClick: () -> Unit, + queue: List +) { ScreenScaffold(scrollState = columnState, modifier = modifier) { ScalingLazyColumn(columnState = columnState) { item { @@ -119,7 +217,7 @@ fun HomeScreen( } } item { - if (viewState.queue.isEmpty()) { + if (queue.isEmpty()) { QueueEmpty() } else { Chip( @@ -132,51 +230,6 @@ fun HomeScreen( } } } - AlertDialog( - message = stringResource(R.string.entity_no_featured_podcasts), - showDialog = !haveDismissedDialog && viewState.featuredPodcasts.isEmpty(), - onDismiss = { haveDismissedDialog = true }, - - content = { - if (viewState.podcastCategoryFilterResult.topPodcasts.isNotEmpty()) { - items(viewState.podcastCategoryFilterResult.topPodcasts.take(3)) { podcast -> - PodcastContent( - podcast = podcast, - downloadItemArtworkPlaceholder = rememberVectorPainter( - image = Icons.Default.MusicNote, - tintColor = Color.Blue, - ), - onClick = { - onTogglePodcastFollowed(podcast) - }, - ) - } - } else { - item { - PlaceholderChip( - contentDescription = "", - colors = ChipDefaults.secondaryChipColors() - ) - } - } - } - ) -} -@Composable -private fun PodcastContent( - podcast: PodcastInfo, - downloadItemArtworkPlaceholder: Painter?, - onClick: () -> Unit -) { - val mediaTitle = podcast.title - - Chip( - label = mediaTitle, - onClick = onClick, - icon = CoilPaintable(podcast.imageUrl, downloadItemArtworkPlaceholder), - largeIcon = true, - colors = ChipDefaults.secondaryChipColors(), - ) } @Composable diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt new file mode 100644 index 0000000000..b4074a711e --- /dev/null +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt @@ -0,0 +1,143 @@ +/* + * 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.library + +/* + * 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. + */ + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo +import com.example.jetcaster.core.data.database.model.asExternalModel +import com.example.jetcaster.core.data.database.model.toPlayerEpisode +import com.example.jetcaster.core.data.domain.PodcastCategoryFilterUseCase +import com.example.jetcaster.core.data.repository.CategoryStore +import com.example.jetcaster.core.data.repository.EpisodeStore +import com.example.jetcaster.core.data.repository.PodcastStore +import com.example.jetcaster.core.data.repository.PodcastsRepository +import com.example.jetcaster.core.model.CategoryTechnology +import com.example.jetcaster.core.model.PlayerEpisode +import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.core.player.EpisodePlayer +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +@HiltViewModel +class LibraryViewModel @Inject constructor( + private val podcastsRepository: PodcastsRepository, + private val episodeStore: EpisodeStore, + private val podcastStore: PodcastStore, + private val episodePlayer: EpisodePlayer, + private val categoryStore: CategoryStore, + private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase +) : ViewModel() { + + private val defaultCategory = categoryStore.getCategory(CategoryTechnology) + private val topPodcastsFlow = defaultCategory.flatMapLatest { + podcastCategoryFilterUseCase(it?.asExternalModel()) + } + + private val followingPodcastListFlow = podcastStore.followedPodcastsSortedByLastEpisode() + + private val queue = episodePlayer.playerState.map { + it.queue + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val latestEpisodeListFlow = podcastStore + .followedPodcastsSortedByLastEpisode() + .flatMapLatest { podcastList -> + if (podcastList.isNotEmpty()) { + combine(podcastList.map { episodeStore.episodesInPodcast(it.podcast.uri, 1) }) { + it.map { episodes -> + episodes.first() + } + } + } else { + flowOf(emptyList()) + } + }.map { list -> + (list.map { it.toPlayerEpisode() }) + } + + val uiState = + combine( + topPodcastsFlow, + followingPodcastListFlow, + latestEpisodeListFlow, + queue + ) { topPodcasts, podcastList, episodeList, queue -> + if (podcastList.isEmpty()) { + LibraryScreenUiState.NoSubscribedPodcast(topPodcasts.topPodcasts) + } else { + LibraryScreenUiState.Ready(podcastList, episodeList, queue) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + LibraryScreenUiState.Loading + ) + + init { + viewModelScope.launch { + podcastsRepository.updatePodcasts(false) + } + } + + fun playEpisode(playerEpisode: PlayerEpisode) { + episodePlayer.play(playerEpisode) + } + + fun onTogglePodcastFollowed(podcastUri: String) { + viewModelScope.launch { + podcastStore.togglePodcastFollowed(podcastUri) + } + } +} + +sealed interface LibraryScreenUiState { + data object Loading : LibraryScreenUiState + data class NoSubscribedPodcast( + val topPodcasts: List + ) : LibraryScreenUiState + data class Ready( + val subscribedPodcastList: List, + val latestEpisodeList: List, + val queue: List + ) : LibraryScreenUiState +}