diff --git a/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt b/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt index 034be43c13f..3dda946d33d 100644 --- a/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt +++ b/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt @@ -5,18 +5,36 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.Toast import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.fragment.compose.content import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground +import au.com.shiftyjelly.pocketcasts.compose.theme +import au.com.shiftyjelly.pocketcasts.settings.HelpPage +import au.com.shiftyjelly.pocketcasts.settings.LogsPage +import au.com.shiftyjelly.pocketcasts.settings.status.StatusPage import au.com.shiftyjelly.pocketcasts.views.fragments.BaseDialogFragment import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch @AndroidEntryPoint class WinbackFragment : BaseDialogFragment() { @@ -29,6 +47,9 @@ class WinbackFragment : BaseDialogFragment() { themeType = theme.activeTheme, ) { val navController = rememberNavController() + + DialogTintEffect(navController) + NavHost( navController = navController, startDestination = WinbackNavRoutes.WinbackOffer, @@ -64,10 +85,25 @@ class WinbackFragment : BaseDialogFragment() { ) } composable(WinbackNavRoutes.HelpAndFeedback) { - HelpAndFeedbackPage( + HelpPage( + activity = requireActivity(), + onShowLogs = { navController.navigate(WinbackNavRoutes.SupportLogs) }, + onShowStatusPage = { navController.navigate(WinbackNavRoutes.StatusCheck) }, onGoBack = { navController.popBackStack() }, ) } + composable(WinbackNavRoutes.SupportLogs) { + LogsPage( + bottomInset = 0.dp, + onBackPressed = { navController.popBackStack() }, + ) + } + composable(WinbackNavRoutes.StatusCheck) { + StatusPage( + bottomInset = 0.dp, + onBackPressed = { navController.popBackStack() }, + ) + } composable(WinbackNavRoutes.CancelConfirmation) { CancelConfirmationPage( onKeepSubscription = { dismiss() }, @@ -77,6 +113,36 @@ class WinbackFragment : BaseDialogFragment() { } } } + + @Composable + private fun DialogTintEffect( + navController: NavHostController, + ) { + var isBackgroundStyled by remember { mutableStateOf(false) } + var isNavBarWhite by remember { mutableStateOf(false) } + LaunchedEffect(navController) { + navController.currentBackStackEntryFlow.collect { entry -> + isBackgroundStyled = entry.destination.route in routesWithAppBar + isNavBarWhite = entry.destination.route == WinbackNavRoutes.HelpAndFeedback + } + } + val backgroundTint by animateColorAsState( + animationSpec = colorAnimationSpec, + targetValue = with(MaterialTheme.theme.colors) { if (isBackgroundStyled) secondaryUi01 else primaryUi01 }, + ) + val navigationBarTint by animateColorAsState( + animationSpec = colorAnimationSpec, + targetValue = with(MaterialTheme.theme.colors) { if (isNavBarWhite) Color.White else primaryUi01 }, + ) + LaunchedEffect(Unit) { + launch { + snapshotFlow { backgroundTint }.collect { tint -> setBackgroundTint(tint.toArgb()) } + } + launch { + snapshotFlow { navigationBarTint }.collect { tint -> setNavigationBarTint(tint.toArgb()) } + } + } + } } private object WinbackNavRoutes { @@ -84,26 +150,33 @@ private object WinbackNavRoutes { const val OfferClaimed = "OfferClaimed" const val AvailablePlans = "AvailablePlans" const val HelpAndFeedback = "HelpAndFeedback" + const val SupportLogs = "SupportLogs" + const val StatusCheck = "StatusCheck" const val CancelConfirmation = "CancelConfirmation" } -private val animationSpec = tween(350) +private val routesWithAppBar = listOf( + WinbackNavRoutes.HelpAndFeedback, + WinbackNavRoutes.SupportLogs, + WinbackNavRoutes.StatusCheck, +) + +private val colorAnimationSpec = tween(350) +private val intOffsetAnimationSpec = tween(350) private fun AnimatedContentTransitionScope.slideInToStart() = slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Start, - animationSpec = animationSpec, + animationSpec = intOffsetAnimationSpec, ) private fun AnimatedContentTransitionScope.slideOutToStart() = slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Start, - animationSpec = animationSpec, + animationSpec = intOffsetAnimationSpec, ) - private fun AnimatedContentTransitionScope.slideInToEnd() = slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.End, - animationSpec = animationSpec, + animationSpec = intOffsetAnimationSpec, ) - private fun AnimatedContentTransitionScope.slideOutToEnd() = slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.End, - animationSpec = animationSpec, + animationSpec = intOffsetAnimationSpec, ) diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/HelpPage.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/HelpPage.kt index 3281de58f1b..78ec3ae6ff4 100644 --- a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/HelpPage.kt +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/HelpPage.kt @@ -90,10 +90,10 @@ fun HelpPage( onShowLogs: () -> Unit, onShowStatusPage: () -> Unit, onGoBack: () -> Unit, - onWebViewCreated: (WebView) -> Unit, - onWebViewDisposed: (WebView) -> Unit, modifier: Modifier = Modifier, viewModel: HelpViewModel = hiltViewModel(), + onWebViewCreated: (WebView) -> Unit = {}, + onWebViewDisposed: (WebView) -> Unit = {}, ) { val scope = rememberCoroutineScope() val state by viewModel.uiState.collectAsState() diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/LogsFragment.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/LogsFragment.kt index 88798ff77c7..ffefd44b8be 100644 --- a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/LogsFragment.kt +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/LogsFragment.kt @@ -3,12 +3,16 @@ package au.com.shiftyjelly.pocketcasts.settings import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons @@ -21,6 +25,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext @@ -65,8 +70,8 @@ class LogsFragment : BaseFragment() { AppThemeWithBackground(theme.activeTheme) { val bottomInset = settings.bottomInset.collectAsStateWithLifecycle(initialValue = 0) LogsPage( - onBackPressed = ::closeFragment, bottomInset = bottomInset.value.pxToDp(LocalContext.current).dp, + onBackPressed = ::closeFragment, ) } } @@ -77,13 +82,14 @@ class LogsFragment : BaseFragment() { } @Composable -private fun LogsPage( - onBackPressed: () -> Unit, +fun LogsPage( bottomInset: Dp, + onBackPressed: () -> Unit, ) { val viewModel = hiltViewModel() val state by viewModel.state.collectAsState() val logs = state.logs + val logLines = state.logLines val clipboardManager = LocalClipboardManager.current val context = LocalContext.current @@ -92,7 +98,7 @@ private fun LogsPage( onCopyToClipboard = { logs?.let { clipboardManager.setText(AnnotatedString(it)) } }, onShareLogs = { viewModel.shareLogs(context) }, includeAppBar = !Util.isAutomotive(context), - logs = logs, + logLines = logLines, bottomInset = bottomInset, ) } @@ -102,11 +108,11 @@ private fun LogsContent( onBackPressed: () -> Unit, onCopyToClipboard: () -> Unit, onShareLogs: () -> Unit, - logs: String?, + logLines: List, includeAppBar: Boolean, bottomInset: Dp, ) { - val logScrollState = rememberScrollState(0) + val logScrollState = rememberLazyListState() Column { if (includeAppBar) { val coroutineScope = rememberCoroutineScope() @@ -115,37 +121,57 @@ private fun LogsContent( onCopyToClipboard = onCopyToClipboard, onShareLogs = onShareLogs, onScrollToTop = { - coroutineScope.launch { - logScrollState.animateScrollTo(0) + if (logLines.isNotEmpty()) { + coroutineScope.launch { + logScrollState.animateScrollToItem(0) + } } }, onScrollToBottom = { - coroutineScope.launch { - logScrollState.animateScrollTo(Int.MAX_VALUE) + if (logLines.isNotEmpty()) { + coroutineScope.launch { + logScrollState.animateScrollToItem(Int.MAX_VALUE) + } } }, - logsAvailable = logs != null, + logsAvailable = logLines.isNotEmpty(), ) } - Column( + Box( modifier = Modifier - .verticalScroll(logScrollState) - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = bottomInset) - .fillMaxWidth(), + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), ) { - if (logs == null) { - LoadingView() + if (logLines.isEmpty()) { + LoadingView( + modifier = Modifier.align(Alignment.Center), + ) } else { SelectionContainer { - TextP60(logs) + LazyColumn( + state = logScrollState, + modifier = Modifier.fillMaxWidth(), + ) { + item { + Spacer(modifier = Modifier.size(16.dp)) + } + items(logLines) { log -> + TextP60(log) + } + item { + Spacer(modifier = Modifier.size(bottomInset)) + } + } } } } } // scroll to the end to show the latest logs - LaunchedEffect(logs) { - logScrollState.scrollTo(logScrollState.maxValue) + LaunchedEffect(logLines) { + if (logLines.isNotEmpty()) { + logScrollState.scrollToItem(Int.MAX_VALUE) + } } } @@ -212,7 +238,11 @@ private fun LogsContentPreview(@PreviewParameter(ThemePreviewParameterProvider:: onBackPressed = {}, onCopyToClipboard = {}, onShareLogs = {}, - logs = "This is a preview", + logLines = listOf( + "This is a preview", + "Of some logs", + "In our app", + ), includeAppBar = true, bottomInset = 0.dp, ) @@ -227,7 +257,7 @@ private fun LogsContentLoadingPreview(@PreviewParameter(ThemePreviewParameterPro onBackPressed = {}, onCopyToClipboard = {}, onShareLogs = {}, - logs = null, + logLines = emptyList(), includeAppBar = true, bottomInset = 0.dp, ) diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/status/StatusFragment.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/status/StatusFragment.kt index b9aba40241e..4f014098cd4 100644 --- a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/status/StatusFragment.kt +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/status/StatusFragment.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.viewModels import androidx.fragment.compose.content +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground import au.com.shiftyjelly.pocketcasts.compose.bars.ThemedTopAppBar @@ -63,8 +64,8 @@ class StatusFragment : BaseFragment() { AppThemeWithBackground(theme.activeTheme) { StatusPage( viewModel = viewModel, - onBackPressed = { closeFragment() }, bottomInset = bottomInset.value.pxToDp(LocalContext.current).dp, + onBackPressed = { closeFragment() }, ) } } @@ -76,9 +77,9 @@ class StatusFragment : BaseFragment() { @Composable fun StatusPage( - viewModel: StatusViewModel, - onBackPressed: () -> Unit, bottomInset: Dp, + onBackPressed: () -> Unit, + viewModel: StatusViewModel = hiltViewModel(), ) { LazyColumn( contentPadding = PaddingValues(bottom = bottomInset), diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/viewmodel/LogsViewModel.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/viewmodel/LogsViewModel.kt index e0eda6632a4..e01b488163c 100644 --- a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/viewmodel/LogsViewModel.kt +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/viewmodel/LogsViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @HiltViewModel class LogsViewModel @Inject constructor( @@ -19,17 +20,17 @@ class LogsViewModel @Inject constructor( data class State( val logs: String?, + val logLines: List, ) - private val _state = MutableStateFlow(State(null)) + private val _state = MutableStateFlow(State(null, emptyList())) val state = _state.asStateFlow() init { viewModelScope.launch { - _state.update { - val logs = support.getLogs() - it.copy(logs = logs) - } + val logs = support.getLogs() + val logLines = withContext(Dispatchers.Default) { logs.split('\n') } + _state.update { it.copy(logs = logs, logLines = logLines) } } }