Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement watch downloads screen #854

Merged
merged 4 commits into from
Mar 31, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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
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 java.util.Date
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<DownloadsScreenViewModel>()
val state by viewModel.stateFlow.collectAsState()
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that collectAsStateWithLifecycle() is available with the recent lifecycle version bump, wdyt about replacing collectAsState() with it?


Content(columnState, state, onItemClick)
}

@Composable
private fun Content(
columnState: ScalingLazyColumnState,
episodes: List<Episode>,
onItemClick: (Episode) -> Unit,
) {
ScalingLazyColumn(
columnState = columnState,
) {
item {
Text(
text = stringResource(
if (episodes.isEmpty()) {
LR.string.profile_empty_downloaded
} else {
LR.string.downloads
}
),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.button,
modifier = Modifier
.padding(horizontal = 10.dp)
.padding(bottom = 12.dp)
.fillMaxWidth(),
)
}

items(episodes) { 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")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: I see that if the duration is less than an hour, mins are displayed slightly differently:

Screenshot 2023-03-31 at 9 08 26 AM

No need to fix it right now, just mentioning it as I noticed it in Figma.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I'll confirm with Adam.

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(
widthDp = 200,
heightDp = 200,
uiMode = Configuration.UI_MODE_TYPE_WATCH,
)
@Composable
private fun DownloadsScreenPreview() {
WearAppTheme(Theme.ThemeType.DARK) {
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,
),
)
)
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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() },
)
}
Expand All @@ -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<EpisodeViewModel>(episodeScreenViewModelStoreOwner)

ObtainConfirmationScreen(
Expand All @@ -77,7 +83,7 @@ object EpisodeScreenFlow {
viewModel.deleteDownloadedEpisode()
navController.navigate(deleteDownloadNotificationScreen) {
popUpTo(episodeScreen) {
inclusive = true
inclusive = false
}
}
},
Expand Down