From 21a02c33ae440bcb130f5463f7e7f99a352cb04c Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Wed, 29 Mar 2023 22:19:01 -0400 Subject: [PATCH 1/4] Implement watch downloads screen --- .../pocketcasts/wear/MainActivity.kt | 14 +- .../pocketcasts/wear/ui/DownloadsScreen.kt | 24 --- .../pocketcasts/wear/ui/WatchListScreen.kt | 1 + .../wear/ui/downloads/DownloadsScreen.kt | 166 ++++++++++++++++++ .../ui/downloads/DownloadsScreenViewModel.kt | 20 +++ 5 files changed, 199 insertions(+), 26 deletions(-) delete mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/DownloadsScreen.kt create mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt create mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreenViewModel.kt diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/MainActivity.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/MainActivity.kt index 2fc1e3fdbc2..7e8562cd47b 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/MainActivity.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/MainActivity.kt @@ -11,12 +11,12 @@ import androidx.navigation.navArgument import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import au.com.shiftyjelly.pocketcasts.ui.theme.Theme import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme -import au.com.shiftyjelly.pocketcasts.wear.ui.DownloadsScreen import au.com.shiftyjelly.pocketcasts.wear.ui.FilesScreen import au.com.shiftyjelly.pocketcasts.wear.ui.FiltersScreen import au.com.shiftyjelly.pocketcasts.wear.ui.UpNextScreen import au.com.shiftyjelly.pocketcasts.wear.ui.WatchListScreen import au.com.shiftyjelly.pocketcasts.wear.ui.authenticationGraph +import au.com.shiftyjelly.pocketcasts.wear.ui.downloads.DownloadsScreen import au.com.shiftyjelly.pocketcasts.wear.ui.episode.EpisodeScreenFlow import au.com.shiftyjelly.pocketcasts.wear.ui.episode.EpisodeScreenFlow.episodeGraph import au.com.shiftyjelly.pocketcasts.wear.ui.player.NowPlayingScreen @@ -113,7 +113,17 @@ fun WearApp(themeType: Theme.ThemeType) { ) composable(FiltersScreen.route) { FiltersScreen() } - composable(DownloadsScreen.route) { DownloadsScreen() } + + scrollable(DownloadsScreen.route) { + DownloadsScreen( + columnState = it.columnState, + onItemClick = { episode -> + val route = EpisodeScreenFlow.navigateRoute(episodeUuid = episode.uuid) + navController.navigate(route) + } + ) + } + composable(FilesScreen.route) { FilesScreen() } authenticationGraph(navController) diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/DownloadsScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/DownloadsScreen.kt deleted file mode 100644 index 6684c725799..00000000000 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/DownloadsScreen.kt +++ /dev/null @@ -1,24 +0,0 @@ -package au.com.shiftyjelly.pocketcasts.wear.ui - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.wear.compose.material.MaterialTheme -import androidx.wear.compose.material.Text -import au.com.shiftyjelly.pocketcasts.localization.R as LR - -object DownloadsScreen { - const val route = "downloads_screen" -} - -@Composable -fun DownloadsScreen() { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - color = MaterialTheme.colors.primary, - text = stringResource(LR.string.downloads) - ) -} diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreen.kt index 0b3828d244b..35b2ad6229e 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreen.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreen.kt @@ -36,6 +36,7 @@ import au.com.shiftyjelly.pocketcasts.ui.theme.Theme import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme import au.com.shiftyjelly.pocketcasts.wear.theme.theme import au.com.shiftyjelly.pocketcasts.wear.ui.component.WatchListChip +import au.com.shiftyjelly.pocketcasts.wear.ui.downloads.DownloadsScreen import au.com.shiftyjelly.pocketcasts.wear.ui.player.NowPlayingScreen import au.com.shiftyjelly.pocketcasts.wear.ui.podcasts.PodcastsScreen import au.com.shiftyjelly.pocketcasts.images.R as IR diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt new file mode 100644 index 00000000000..f7be9548126 --- /dev/null +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt @@ -0,0 +1,166 @@ +package au.com.shiftyjelly.pocketcasts.wear.ui.downloads + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Text +import au.com.shiftyjelly.pocketcasts.compose.components.PodcastImage +import au.com.shiftyjelly.pocketcasts.localization.helper.TimeHelper +import au.com.shiftyjelly.pocketcasts.models.entity.Episode +import au.com.shiftyjelly.pocketcasts.ui.theme.Theme +import au.com.shiftyjelly.pocketcasts.utils.extensions.toLocalizedFormatPattern +import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme +import au.com.shiftyjelly.pocketcasts.wear.theme.theme +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.ScalingLazyColumnState +import au.com.shiftyjelly.pocketcasts.images.R as IR +import au.com.shiftyjelly.pocketcasts.localization.R as LR + +object DownloadsScreen { + const val route = "downloads_screen" +} + +@Composable +fun DownloadsScreen( + columnState: ScalingLazyColumnState, + onItemClick: (Episode) -> Unit, +) { + + val viewModel = hiltViewModel() + val state by viewModel.stateFlow.collectAsState() + + ScalingLazyColumn( + columnState = columnState, + ) { + item { + Text( + text = stringResource( + if (state.isEmpty()) { + LR.string.profile_empty_downloaded + } else { + LR.string.downloads + } + ), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.button, + modifier = Modifier + .padding(horizontal = 10.dp) + .fillMaxWidth(), + ) + } + + items(state) { episode -> + Download( + episode = episode, + onClick = { onItemClick(episode) } + ) + } + } +} + +@Composable +private fun Download(episode: Episode, onClick: () -> Unit) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colors.surface) + .clickable { onClick() } + .padding(horizontal = 10.dp) + .fillMaxWidth() + .height(72.dp) + ) { + Row { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PodcastImage( + uuid = episode.podcastUuid, + dropShadow = false, + modifier = Modifier.size(30.dp), + ) + Spacer(Modifier.height(4.dp)) + Icon( + painter = painterResource(IR.drawable.ic_downloaded), + contentDescription = null, + tint = MaterialTheme.theme.colors.support02, + modifier = Modifier.size(12.dp), + ) + } + + Spacer(Modifier.width(6.dp)) + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = episode.title, + lineHeight = 14.sp, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.button.merge( + @Suppress("DEPRECATION") + TextStyle( + platformStyle = PlatformTextStyle( + // So we can align the top of the text as closely as possible to the image + includeFontPadding = false, + ), + ) + ), + maxLines = 2, + ) + val shortDate = episode.publishedDate.toLocalizedFormatPattern("dd MMM") + val timeLeft = TimeHelper.getTimeLeft( + currentTimeMs = episode.playedUpToMs, + durationMs = episode.durationMs.toLong(), + inProgress = episode.isInProgress, + context = LocalContext.current + ).text + Text( + text = "$shortDate • $timeLeft", + color = MaterialTheme.theme.colors.primaryText02, + style = MaterialTheme.typography.caption2 + ) + } + } + } +} + +@Preview +@Composable +private fun DownloadsScreenPreview() { + WearAppTheme(Theme.ThemeType.DARK) { + DownloadsScreen( + columnState = ScalingLazyColumnState(), + onItemClick = {}, + ) + } +} diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreenViewModel.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreenViewModel.kt new file mode 100644 index 00000000000..1bff6244099 --- /dev/null +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreenViewModel.kt @@ -0,0 +1,20 @@ +package au.com.shiftyjelly.pocketcasts.wear.ui.downloads + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.reactive.asFlow +import javax.inject.Inject + +@HiltViewModel +class DownloadsScreenViewModel @Inject constructor( + episodeManager: EpisodeManager, +) : ViewModel() { + + val stateFlow = episodeManager.observeDownloadEpisodes() + .asFlow() + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) +} From 5f9a24024e30bd5b6a6dc63d4a941eb97ca183a1 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Thu, 30 Mar 2023 09:45:43 -0400 Subject: [PATCH 2/4] After deleting download, only pop back to episode screen Before this change, we were popping back to the screen before the episode screen. --- .../pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt index 9b87f3d0df5..dee59c44159 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt @@ -77,7 +77,7 @@ object EpisodeScreenFlow { viewModel.deleteDownloadedEpisode() navController.navigate(deleteDownloadNotificationScreen) { popUpTo(episodeScreen) { - inclusive = true + inclusive = false } } }, From 4ba277842ff34e45c7aa07bc7f10e5c01d15a0e4 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Thu, 30 Mar 2023 10:00:06 -0400 Subject: [PATCH 3/4] Use remember when getting backstack entry --- .../pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt index dee59c44159..70aeb1140d9 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreenFlow.kt @@ -1,6 +1,7 @@ package au.com.shiftyjelly.pocketcasts.wear.ui.episode import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -58,8 +59,11 @@ object EpisodeScreenFlow { composable(upNextOptionsScreen) { it.viewModel.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + val episodeScreenBackStackEntry = remember(it.backStackEntry) { + navController.getBackStackEntry(episodeScreen) + } UpNextOptionsScreen( - episodeScreenViewModelStoreOwner = navController.getBackStackEntry(episodeScreen), // Reuse view model from EpisodeScreen + episodeScreenViewModelStoreOwner = episodeScreenBackStackEntry, // Reuse view model from EpisodeScreen onComplete = { navController.popBackStack() }, ) } @@ -68,7 +72,9 @@ object EpisodeScreenFlow { it.viewModel.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off // Reuse view model from EpisodeScreen - val episodeScreenViewModelStoreOwner = navController.getBackStackEntry(episodeScreen) + val episodeScreenViewModelStoreOwner = remember(it.backStackEntry) { + navController.getBackStackEntry(episodeScreen) + } val viewModel = hiltViewModel(episodeScreenViewModelStoreOwner) ObtainConfirmationScreen( From 102cbdd8665c7e9efe5fd0aa663163ecdb9472f7 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Thu, 30 Mar 2023 11:25:41 -0400 Subject: [PATCH 4/4] Fix DownloadScreen preview --- .../wear/ui/downloads/DownloadsScreen.kt | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt index f7be9548126..20cbd6755ac 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/downloads/DownloadsScreen.kt @@ -1,5 +1,6 @@ package au.com.shiftyjelly.pocketcasts.wear.ui.downloads +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -42,6 +43,7 @@ import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme import au.com.shiftyjelly.pocketcasts.wear.theme.theme import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumnState +import java.util.Date import au.com.shiftyjelly.pocketcasts.images.R as IR import au.com.shiftyjelly.pocketcasts.localization.R as LR @@ -58,13 +60,22 @@ fun DownloadsScreen( val viewModel = hiltViewModel() val state by viewModel.stateFlow.collectAsState() + Content(columnState, state, onItemClick) +} + +@Composable +private fun Content( + columnState: ScalingLazyColumnState, + episodes: List, + onItemClick: (Episode) -> Unit, +) { ScalingLazyColumn( columnState = columnState, ) { item { Text( text = stringResource( - if (state.isEmpty()) { + if (episodes.isEmpty()) { LR.string.profile_empty_downloaded } else { LR.string.downloads @@ -74,11 +85,12 @@ fun DownloadsScreen( style = MaterialTheme.typography.button, modifier = Modifier .padding(horizontal = 10.dp) + .padding(bottom = 12.dp) .fillMaxWidth(), ) } - items(state) { episode -> + items(episodes) { episode -> Download( episode = episode, onClick = { onItemClick(episode) } @@ -154,13 +166,35 @@ private fun Download(episode: Episode, onClick: () -> Unit) { } } -@Preview +@Preview( + widthDp = 200, + heightDp = 200, + uiMode = Configuration.UI_MODE_TYPE_WATCH, +) @Composable private fun DownloadsScreenPreview() { WearAppTheme(Theme.ThemeType.DARK) { - DownloadsScreen( + Content( columnState = ScalingLazyColumnState(), onItemClick = {}, + episodes = listOf( + Episode( + uuid = "57853d71-30ac-4477-af73-e8fe2b1d4dda", + podcastUuid = "b643cb50-2c52-013b-ef7a-0acc26574db2", + title = "Such a great episode title, but it's so long that it is definitely going to be more than two lines", + publishedDate = Date(), + playedUpTo = 0.0, + duration = 20.0, + ), + Episode( + uuid = "c146e703-e408-4979-852c-f9927ce19ef7", + podcastUuid = "3df2e780-0063-0135-ec79-4114446340cb", + title = "1 line title", + publishedDate = Date(), + playedUpTo = 0.0, + duration = 20.0, + ), + ) ) } }