From 85e4f599b23ef91b3ccbd9eed1b55039c4f369b9 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Fri, 31 Mar 2023 21:21:08 -0400 Subject: [PATCH 1/3] Add permissions from phone app --- wear/src/main/AndroidManifest.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 10404e7dc41..50038b412c0 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -5,6 +5,15 @@ + + + + + + + + + From b51fb62f1be966c23010d2e1da1dcd69526b6407 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Fri, 31 Mar 2023 21:26:37 -0400 Subject: [PATCH 2/3] Add settings screen for watch --- .../images/src/main/res/drawable/signin.xml | 16 ++ .../src/main/res/values/strings.xml | 5 + .../repositories/playback/PlaybackManager.kt | 8 +- .../pocketcasts/wear/MainActivity.kt | 47 ++++- .../pocketcasts/wear/theme/WearColors.kt | 1 + .../pocketcasts/wear/ui/SettingsScreen.kt | 180 ++++++++++++++++++ .../pocketcasts/wear/ui/SettingsViewModel.kt | 62 ++++++ .../pocketcasts/wear/ui/WatchListScreen.kt | 30 +-- .../wear/ui/component/ChipScreenHeaders.kt | 38 ++++ .../ObtainConfirmationScreen.kt | 2 +- .../wear/ui/episode/EpisodeScreen.kt | 5 +- .../wear/ui/episode/EpisodeScreenFlow.kt | 19 ++ .../wear/ui/episode/EpisodeViewModel.kt | 20 +- .../wear/ui/player/NowPlayingScreen.kt | 3 +- .../wear/ui/player/NowPlayingViewModel.kt | 23 ++- .../ui/player/StreamingConfirmationScreen.kt | 28 +++ 16 files changed, 450 insertions(+), 37 deletions(-) create mode 100644 modules/services/images/src/main/res/drawable/signin.xml create mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsScreen.kt create mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsViewModel.kt create mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt rename wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/{episode => component}/ObtainConfirmationScreen.kt (98%) create mode 100644 wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/StreamingConfirmationScreen.kt diff --git a/modules/services/images/src/main/res/drawable/signin.xml b/modules/services/images/src/main/res/drawable/signin.xml new file mode 100644 index 00000000000..425c7bcf334 --- /dev/null +++ b/modules/services/images/src/main/res/drawable/signin.xml @@ -0,0 +1,16 @@ + + + + diff --git a/modules/services/localization/src/main/res/values/strings.xml b/modules/services/localization/src/main/res/values/strings.xml index f665a5443fc..cdf58b89aff 100644 --- a/modules/services/localization/src/main/res/values/strings.xml +++ b/modules/services/localization/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ + Account Add to Up Next Archive Unarchive @@ -25,6 +26,8 @@ Go to podcast Go to files In progress + Log in + Log out Mark as played Mark as unplayed Mark played @@ -192,6 +195,7 @@ @string/youre_not_on_wifi @string/youre_on_metered_wifi Streaming this episode will use data. + Streaming will use data. Proceed? Stream Anyway No podcasts found @@ -1054,6 +1058,7 @@ @string/play_next @string/playback_speed @string/star + Metered data warning No thanks New Episodes Actions diff --git a/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PlaybackManager.kt b/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PlaybackManager.kt index a4ae3eccc3d..e258e22191c 100644 --- a/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PlaybackManager.kt +++ b/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PlaybackManager.kt @@ -1366,7 +1366,13 @@ open class PlaybackManager @Inject constructor( episodeSubscription?.dispose() if (!episode.isDownloaded) { - if (!Util.isCarUiMode(application) && settings.warnOnMeteredNetwork() && !Network.isUnmeteredConnection(application) && !forceStream && play) { + if (!Util.isCarUiMode(application) && + !Util.isWearOs(application) && // The watch handles these warnings before this is called + settings.warnOnMeteredNetwork() && + !Network.isUnmeteredConnection(application) && + !forceStream && + play + ) { sendDataWarningNotification(episode) val previousPlaybackState = playbackStateRelay.blockingFirst() val playbackState = PlaybackState( 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 7e8562cd47b..b185634116f 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 @@ -4,8 +4,11 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavType import androidx.navigation.navArgument import androidx.wear.compose.navigation.rememberSwipeDismissableNavController @@ -13,13 +16,17 @@ import au.com.shiftyjelly.pocketcasts.ui.theme.Theme import au.com.shiftyjelly.pocketcasts.wear.theme.WearAppTheme import au.com.shiftyjelly.pocketcasts.wear.ui.FilesScreen import au.com.shiftyjelly.pocketcasts.wear.ui.FiltersScreen +import au.com.shiftyjelly.pocketcasts.wear.ui.SettingsScreen 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.authenticationSubGraph 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 +import au.com.shiftyjelly.pocketcasts.wear.ui.player.NowPlayingViewModel +import au.com.shiftyjelly.pocketcasts.wear.ui.player.StreamingConfirmationScreen import au.com.shiftyjelly.pocketcasts.wear.ui.podcast.PodcastScreen import au.com.shiftyjelly.pocketcasts.wear.ui.podcasts.PodcastsScreen import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel @@ -59,12 +66,41 @@ fun WearApp(themeType: Theme.ThemeType) { WatchListScreen(navController::navigate, it.scrollableState) } - composable(NowPlayingScreen.route) { viewModel -> - viewModel.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + composable(NowPlayingScreen.route) { + it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + + // Listen for results from streaming confirmation screen + navController.currentBackStackEntry?.savedStateHandle + ?.getStateFlow(StreamingConfirmationScreen.resultKey, null) + ?.collectAsStateWithLifecycle()?.value?.let { streamingConfirmationResult -> + val viewModel = hiltViewModel() + LaunchedEffect(streamingConfirmationResult) { + viewModel.onStreamingConfirmationResult(streamingConfirmationResult) + // Clear result once consumed + navController.currentBackStackEntry?.savedStateHandle + ?.remove(StreamingConfirmationScreen.resultKey) + } + } + NowPlayingScreen( navigateToEpisode = { episodeUuid -> navController.navigate(EpisodeScreenFlow.navigateRoute(episodeUuid)) }, + showStreamingConfirmation = { navController.navigate(StreamingConfirmationScreen.route) }, + ) + } + + composable(StreamingConfirmationScreen.route) { + it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + + StreamingConfirmationScreen( + onFinished = { result -> + navController.previousBackStackEntry?.savedStateHandle?.set( + StreamingConfirmationScreen.resultKey, + result + ) + navController.popBackStack() + }, ) } @@ -126,6 +162,13 @@ fun WearApp(themeType: Theme.ThemeType) { composable(FilesScreen.route) { FilesScreen() } + scrollable(SettingsScreen.route) { + SettingsScreen( + scrollState = it.columnState, + signInClick = { navController.navigate(authenticationSubGraph) }, + ) + } + authenticationGraph(navController) } } diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/theme/WearColors.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/theme/WearColors.kt index 23436af6d60..5ccd72eaa71 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/theme/WearColors.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/theme/WearColors.kt @@ -5,5 +5,6 @@ import androidx.compose.ui.graphics.Color object WearColors { val FF202124 = Color(0xFF202124) val FFA1E7B0 = Color(0xFFA1E7B0) + val FFBDC1C6 = Color(0xFFBDC1C6) val FFDADCE0 = Color(0xFFDADCE0) } diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsScreen.kt new file mode 100644 index 00000000000..8224a263df5 --- /dev/null +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsScreen.kt @@ -0,0 +1,180 @@ +package au.com.shiftyjelly.pocketcasts.wear.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Text +import androidx.wear.compose.material.ToggleChip +import androidx.wear.compose.material.ToggleChipDefaults +import au.com.shiftyjelly.pocketcasts.models.to.SignInState +import au.com.shiftyjelly.pocketcasts.models.to.SubscriptionStatus +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.ChipScreenHeader +import au.com.shiftyjelly.pocketcasts.wear.ui.component.ChipSectionHeader +import au.com.shiftyjelly.pocketcasts.wear.ui.component.WatchListChip +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 SettingsScreen { + const val route = "settings_screen" +} + +@Composable +fun SettingsScreen( + scrollState: ScalingLazyColumnState, + signInClick: () -> Unit, +) { + + val viewModel = hiltViewModel() + val state by viewModel.state.collectAsState() + + Content( + scrollState = scrollState, + state = state, + onWarnOnMeteredChanged = { viewModel.setWarnOnMeteredNetwork(it) }, + signInClick = signInClick, + onSignOutClicked = viewModel::signOut, + ) +} + +@Composable +private fun Content( + scrollState: ScalingLazyColumnState, + state: SettingsViewModel.State, + onWarnOnMeteredChanged: (Boolean) -> Unit, + signInClick: () -> Unit, + onSignOutClicked: () -> Unit, +) { + ScalingLazyColumn(columnState = scrollState) { + + item { + ChipScreenHeader(LR.string.settings) + } + + item { + ToggleChip( + label = stringResource(LR.string.settings_metered_data_warning), + checked = state.showDataWarning, + onCheckedChanged = onWarnOnMeteredChanged, + ) + } + + item { + ChipSectionHeader(LR.string.account) + } + + item { + val signInState = state.signInState + when (signInState) { + is SignInState.SignedIn -> { + WatchListChip( + titleRes = LR.string.log_out, + secondaryLabel = signInState.email, + iconRes = IR.drawable.ic_signout, + onClick = onSignOutClicked, + ) + } + is SignInState.SignedOut -> { + WatchListChip( + titleRes = LR.string.log_in, + iconRes = IR.drawable.signin, + onClick = signInClick, + ) + } + } + } + } +} + +@Composable +private fun ToggleChip( + label: String, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, +) { + val color = MaterialTheme.theme.colors.support05 + ToggleChip( + checked = checked, + onCheckedChange = { onCheckedChanged(it) }, + label = { + Text( + text = label, + style = MaterialTheme.typography.button, + ) + }, + toggleControl = { + Icon( + imageVector = ToggleChipDefaults.switchIcon(checked), + contentDescription = stringResource(if (checked) LR.string.on else LR.string.off), + modifier = Modifier + ) + }, + colors = ToggleChipDefaults.toggleChipColors( + checkedEndBackgroundColor = color.copy(alpha = 0.32f), + checkedToggleControlColor = color, + ), + modifier = Modifier.fillMaxWidth() + ) +} + +@Preview( + widthDp = 200, + heightDp = 200, + uiMode = Configuration.UI_MODE_TYPE_WATCH, +) +@Composable +private fun SettingsScreenPreview_unchecked() { + WearAppTheme(Theme.ThemeType.DARK) { + Content( + scrollState = ScalingLazyColumnState(), + state = SettingsViewModel.State( + signInState = SignInState.SignedIn( + email = "matt@pocketcasts.com", + subscriptionStatus = SubscriptionStatus.Free(), + ), + showDataWarning = false, + ), + signInClick = {}, + onWarnOnMeteredChanged = {}, + onSignOutClicked = {} + + ) + } +} + +@Preview( + widthDp = 200, + heightDp = 200, + uiMode = Configuration.UI_MODE_TYPE_WATCH, +) +@Composable +private fun SettingsScreenPreview_checked() { + WearAppTheme(Theme.ThemeType.DARK) { + Content( + scrollState = ScalingLazyColumnState(), + state = SettingsViewModel.State( + signInState = SignInState.SignedIn( + email = "matt@pocketcasts.com", + subscriptionStatus = SubscriptionStatus.Free(), + ), + showDataWarning = true, + ), + signInClick = {}, + onWarnOnMeteredChanged = {}, + onSignOutClicked = {} + + ) + } +} diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsViewModel.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsViewModel.kt new file mode 100644 index 00000000000..d47233ea109 --- /dev/null +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/SettingsViewModel.kt @@ -0,0 +1,62 @@ +package au.com.shiftyjelly.pocketcasts.wear.ui + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import au.com.shiftyjelly.pocketcasts.models.to.SignInState +import au.com.shiftyjelly.pocketcasts.preferences.Settings +import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager +import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val playbackManager: PlaybackManager, + private val userManager: UserManager, + private val settings: Settings, +) : ViewModel() { + + data class State( + val signInState: SignInState, + val showDataWarning: Boolean, + ) + + private val _state = MutableStateFlow( + State( + signInState = userManager.getSignInState().blockingFirst(), + showDataWarning = settings.warnOnMeteredNetwork(), + ) + ) + val state = _state.asStateFlow() + + init { + viewModelScope.launch { + userManager.getSignInState() + .asFlow() + .collectLatest { signInState -> + _state.update { it.copy(signInState = signInState) } + } + } + } + + fun setWarnOnMeteredNetwork(warnOnMeteredNetwork: Boolean) { + settings.setWarnOnMeteredNetwork(warnOnMeteredNetwork) + _state.update { it.copy(showDataWarning = warnOnMeteredNetwork) } + } + + fun signOut() { + userManager.signOut( + playbackManager = playbackManager, + wasInitiatedByUser = true + ) + } +} 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 35b2ad6229e..1317200a1f7 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 @@ -51,11 +51,9 @@ object WatchListScreen { fun WatchListScreen( navigateToRoute: (String) -> Unit, scrollState: ScalingLazyListState, - viewModel: WatchListViewModel = hiltViewModel(), upNextViewModel: UpNextViewModel = hiltViewModel(), ) { - val signInState by viewModel.signInState.subscribeAsState(null) val upNextState by upNextViewModel.upNextQueue.subscribeAsState(null) ScalingLazyColumn( @@ -72,26 +70,6 @@ fun WatchListScreen( ) } - item { - when (signInState?.isSignedIn) { - true -> { - WatchListChip( - titleRes = LR.string.sign_out, - iconRes = IR.drawable.ic_signout, - onClick = viewModel::signOut, - ) - } - false -> { - WatchListChip( - titleRes = LR.string.sign_in, - iconRes = IR.drawable.ic_profile, - onClick = { navigateToRoute(authenticationSubGraph) }, - ) - } - null -> { /* Do nothing */ } - } - } - item { WatchListChip( titleRes = LR.string.player_tab_playing_wide, @@ -140,6 +118,14 @@ fun WatchListScreen( onClick = { navigateToRoute(FilesScreen.route) } ) } + + item { + WatchListChip( + titleRes = LR.string.settings, + iconRes = IR.drawable.ic_profile_settings, + onClick = { navigateToRoute(SettingsScreen.route) } + ) + } } } diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt new file mode 100644 index 00000000000..575600f0b1e --- /dev/null +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt @@ -0,0 +1,38 @@ +package au.com.shiftyjelly.pocketcasts.wear.ui.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Text +import au.com.shiftyjelly.pocketcasts.wear.theme.WearColors + +@Composable +fun ChipScreenHeader(@StringRes text: Int, modifier: Modifier = Modifier) { + Header( + text, + modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ) + ) +} + +@Composable +fun ChipSectionHeader(@StringRes text: Int, modifier: Modifier = Modifier) { + Header(text, modifier.padding(16.dp)) +} + +@Composable +private fun Header(@StringRes text: Int, modifier: Modifier = Modifier) { + Text( + text = stringResource(text), + color = WearColors.FFBDC1C6, + style = MaterialTheme.typography.button, + modifier = modifier + ) +} diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/ObtainConfirmationScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ObtainConfirmationScreen.kt similarity index 98% rename from wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/ObtainConfirmationScreen.kt rename to wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ObtainConfirmationScreen.kt index 38f5cc6dcba..d7d58bff5d3 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/ObtainConfirmationScreen.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ObtainConfirmationScreen.kt @@ -1,4 +1,4 @@ -package au.com.shiftyjelly.pocketcasts.wear.ui.episode +package au.com.shiftyjelly.pocketcasts.wear.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreen.kt index b906c13b1ae..412063c695d 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreen.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeScreen.kt @@ -38,6 +38,7 @@ fun EpisodeScreen( navigateToUpNextOptions: () -> Unit, navigateToConfirmDeleteDownload: () -> Unit, navigateToRemoveFromUpNextNotification: () -> Unit, + navigateToStreamingConfirmation: () -> Unit, ) { val viewModel = hiltViewModel() @@ -119,9 +120,9 @@ fun EpisodeScreen( backgroundColor = state.tintColor, onClick = { if (state.isPlayingEpisode) { - viewModel.pause() + viewModel.onPauseClicked() } else { - viewModel.play() + viewModel.onPlayClicked(navigateToStreamingConfirmation) } }, isPlaying = state.isPlayingEpisode, 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 70aeb1140d9..cf1edf04821 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,16 +1,20 @@ package au.com.shiftyjelly.pocketcasts.wear.ui.episode import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.navigation import androidx.navigation.navArgument +import au.com.shiftyjelly.pocketcasts.wear.ui.component.ObtainConfirmationScreen +import au.com.shiftyjelly.pocketcasts.wear.ui.player.StreamingConfirmationScreen import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel import com.google.android.horologist.compose.navscaffold.composable @@ -48,12 +52,27 @@ object EpisodeScreenFlow { verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top) ) ) { + + // Listen for results from streaming confirmation screen + navController.currentBackStackEntry?.savedStateHandle + ?.getStateFlow(StreamingConfirmationScreen.resultKey, null) + ?.collectAsStateWithLifecycle()?.value?.let { streamingConfirmationResult -> + val viewModel = hiltViewModel() + LaunchedEffect(streamingConfirmationResult) { + viewModel.onStreamingConfirmationResult(streamingConfirmationResult) + // Clear result once consumed + navController.currentBackStackEntry?.savedStateHandle + ?.remove(StreamingConfirmationScreen.resultKey) + } + } + EpisodeScreen( columnState = it.columnState, navigateToPodcast = navigateToPodcast, navigateToUpNextOptions = { navController.navigate(upNextOptionsScreen) }, navigateToConfirmDeleteDownload = { navController.navigate(deleteDownloadConfirmationScreen) }, navigateToRemoveFromUpNextNotification = { navController.navigate(removeFromUpNextNotificationScreen) }, + navigateToStreamingConfirmation = { navController.navigate(StreamingConfirmationScreen.route) }, ) } diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeViewModel.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeViewModel.kt index 697e4eb671b..2d3ff3d2115 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeViewModel.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/episode/EpisodeViewModel.kt @@ -20,6 +20,7 @@ import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager import au.com.shiftyjelly.pocketcasts.ui.theme.Theme import au.com.shiftyjelly.pocketcasts.ui.theme.ThemeColor +import au.com.shiftyjelly.pocketcasts.wear.ui.player.StreamingConfirmationScreen import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -190,7 +191,22 @@ class EpisodeViewModel @Inject constructor( } } - fun play() { + fun onPlayClicked(showStreamingConfirmation: () -> Unit) { + if (playbackManager.shouldWarnAboutPlayback()) { + showStreamingConfirmation() + } else { + play() + } + } + + fun onStreamingConfirmationResult(result: StreamingConfirmationScreen.Result) { + val confirmedStreaming = result == StreamingConfirmationScreen.Result.CONFIRMED + if (confirmedStreaming && !playbackManager.isPlaying()) { + play() + } + } + + private fun play() { val episode = (stateFlow.value as? State.Loaded)?.episode ?: return viewModelScope.launch { @@ -201,7 +217,7 @@ class EpisodeViewModel @Inject constructor( } } - fun pause() { + fun onPauseClicked() { if ((stateFlow.value as? State.Loaded)?.isPlayingEpisode != true) { Timber.e("Attempted to pause when not playing") return diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingScreen.kt index 075968a4f3a..564c20f01c9 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingScreen.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingScreen.kt @@ -34,6 +34,7 @@ fun NowPlayingScreen( playerViewModel: NowPlayingViewModel = hiltViewModel(), volumeViewModel: PCVolumeViewModel = hiltViewModel(), navigateToEpisode: (episodeUuid: String) -> Unit, + showStreamingConfirmation: () -> Unit, ) { Scaffold( modifier = modifier.fillMaxSize(), @@ -64,7 +65,7 @@ fun NowPlayingScreen( controlButtons = { if (state is NowPlayingViewModel.State.Loaded) { PodcastControlButtons( - onPlayButtonClick = playerViewModel::onPlayButtonClick, + onPlayButtonClick = { playerViewModel.onPlayButtonClick(showStreamingConfirmation) }, onPauseButtonClick = playerViewModel::onPauseButtonClick, playPauseButtonEnabled = true, playing = state.playing, diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingViewModel.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingViewModel.kt index 117f2bab1fe..8663d64f310 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingViewModel.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/NowPlayingViewModel.kt @@ -5,8 +5,6 @@ import androidx.lifecycle.viewModelScope import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsSource import au.com.shiftyjelly.pocketcasts.preferences.Settings import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager -import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager -import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager import com.google.android.horologist.media.ui.components.controls.SeekButtonIncrement import com.google.android.horologist.media.ui.state.model.TrackPositionUiModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,9 +19,7 @@ import kotlin.time.toDuration @HiltViewModel class NowPlayingViewModel @Inject constructor( - private val episodeManager: EpisodeManager, private val playbackManager: PlaybackManager, - private val podcastManager: PodcastManager, settings: Settings, ) : ViewModel() { @@ -72,8 +68,12 @@ class NowPlayingViewModel @Inject constructor( initialValue = State.Loading ) - fun onPlayButtonClick() { - playbackManager.playQueue(AnalyticsSource.WATCH_PLAYER) + fun onPlayButtonClick(showStreamingConfirmation: () -> Unit) { + if (playbackManager.shouldWarnAboutPlayback()) { + showStreamingConfirmation() + } else { + play() + } } fun onPauseButtonClick() { @@ -87,4 +87,15 @@ class NowPlayingViewModel @Inject constructor( fun onSeekForwardButtonClick() { playbackManager.skipForward(AnalyticsSource.WATCH_PLAYER) } + + fun onStreamingConfirmationResult(result: StreamingConfirmationScreen.Result) { + val confirmedStreaming = result == StreamingConfirmationScreen.Result.CONFIRMED + if (confirmedStreaming && !playbackManager.isPlaying()) { + play() + } + } + + private fun play() { + playbackManager.playQueue(AnalyticsSource.WATCH_PLAYER) + } } diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/StreamingConfirmationScreen.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/StreamingConfirmationScreen.kt new file mode 100644 index 00000000000..a8064c18651 --- /dev/null +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/player/StreamingConfirmationScreen.kt @@ -0,0 +1,28 @@ +package au.com.shiftyjelly.pocketcasts.wear.ui.player + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import au.com.shiftyjelly.pocketcasts.wear.ui.component.ObtainConfirmationScreen +import au.com.shiftyjelly.pocketcasts.wear.ui.player.StreamingConfirmationScreen.Result +import au.com.shiftyjelly.pocketcasts.localization.R as LR + +object StreamingConfirmationScreen { + const val route = "streaming_confirmation" + const val resultKey = "${route}_result" + + enum class Result { + CONFIRMED, + CANCELLED + } +} + +@Composable +fun StreamingConfirmationScreen( + onFinished: (Result) -> Unit, +) { + ObtainConfirmationScreen( + text = stringResource(LR.string.stream_warning_summary_short), + onConfirm = { onFinished(Result.CONFIRMED) }, + onCancel = { onFinished(Result.CANCELLED) } + ) +} From 46d22f56c1f945908b96acf678414bc2af7f5704 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Sat, 1 Apr 2023 11:05:08 -0400 Subject: [PATCH 3/3] Use reusable header on download screen --- .../wear/ui/component/ChipScreenHeaders.kt | 16 +++++--- .../wear/ui/downloads/DownloadsScreen.kt | 37 ++++++++----------- .../ui/downloads/DownloadsScreenViewModel.kt | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt index 575600f0b1e..0c78a842f6b 100644 --- a/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt +++ b/wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/component/ChipScreenHeaders.kt @@ -1,10 +1,12 @@ package au.com.shiftyjelly.pocketcasts.wear.ui.component import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding 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.compose.ui.unit.dp import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Text @@ -15,16 +17,16 @@ fun ChipScreenHeader(@StringRes text: Int, modifier: Modifier = Modifier) { Header( text, modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, + start = horizontalPadding, + end = horizontalPadding, + bottom = verticalPadding, ) ) } @Composable fun ChipSectionHeader(@StringRes text: Int, modifier: Modifier = Modifier) { - Header(text, modifier.padding(16.dp)) + Header(text, modifier.padding(vertical = verticalPadding, horizontal = horizontalPadding)) } @Composable @@ -32,7 +34,11 @@ private fun Header(@StringRes text: Int, modifier: Modifier = Modifier) { Text( text = stringResource(text), color = WearColors.FFBDC1C6, + textAlign = TextAlign.Center, style = MaterialTheme.typography.button, - modifier = modifier + modifier = modifier.fillMaxWidth() ) } + +private val horizontalPadding = 10.dp +private val verticalPadding = 14.dp 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 20cbd6755ac..ec2faccf30d 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 @@ -21,10 +21,8 @@ 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 @@ -41,6 +39,7 @@ 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 au.com.shiftyjelly.pocketcasts.wear.ui.component.ChipScreenHeader import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumnState import java.util.Date @@ -66,35 +65,29 @@ fun DownloadsScreen( @Composable private fun Content( columnState: ScalingLazyColumnState, - episodes: List, + episodes: List?, onItemClick: (Episode) -> Unit, ) { ScalingLazyColumn( columnState = columnState, ) { - item { - Text( - text = stringResource( - if (episodes.isEmpty()) { + if (episodes != null) { + item { + ChipScreenHeader( + text = 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) } - ) + items(episodes) { episode -> + Download( + episode = episode, + onClick = { onItemClick(episode) } + ) + } } } } 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 index 1bff6244099..3d96f4dfc5b 100644 --- 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 @@ -16,5 +16,5 @@ class DownloadsScreenViewModel @Inject constructor( val stateFlow = episodeManager.observeDownloadEpisodes() .asFlow() - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + .stateIn(viewModelScope, SharingStarted.Lazily, null) }