From e855a414a43789aa7e0d3b328aced3e101d7c58e Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Mon, 14 Oct 2024 18:07:03 +0800 Subject: [PATCH] Feat(App): Support full screen --- .../fatweb/oxygen/toolbox/icon/OxygenIcons.kt | 4 + .../toolbox/navigation/OxygenNavHost.kt | 20 ++ .../top/fatweb/oxygen/toolbox/ui/OxygenApp.kt | 300 ++++++++++-------- .../oxygen/toolbox/ui/util/LocalFullScreen.kt | 10 + .../oxygen/toolbox/ui/view/ToolViewScreen.kt | 17 +- app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 218 insertions(+), 137 deletions(-) create mode 100644 app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalFullScreen.kt diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt index 4e2bef7..7e22b91 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt @@ -10,6 +10,8 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Fullscreen +import androidx.compose.material.icons.filled.FullscreenExit import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Reorder @@ -43,6 +45,8 @@ object OxygenIcons { val Delete = Icons.Default.Delete val Download = Icons.Default.Download val Error = Icons.Default.Cancel + val FullScreen = Icons.Default.Fullscreen + val FullScreenExit = Icons.Default.FullscreenExit val Home = Icons.Rounded.Home val HomeBorder = Icons.Outlined.Home val Info = Icons.Outlined.Info diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt index f69e8d6..2afc63c 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt @@ -1,9 +1,14 @@ package top.fatweb.oxygen.toolbox.navigation +import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavDestination import androidx.navigation.compose.NavHost import top.fatweb.oxygen.toolbox.ui.OxygenAppState +import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen @Composable fun OxygenNavHost( @@ -16,6 +21,21 @@ fun OxygenNavHost( onShowSnackbar: suspend (message: String, action: String?) -> Boolean ) { val navController = appState.navController + val fullScreen = LocalFullScreen.current + + LaunchedEffect(navController) { + navController.addOnDestinationChangedListener(object : + NavController.OnDestinationChangedListener { + override fun onDestinationChanged( + controller: NavController, + destination: NavDestination, + arguments: Bundle? + ) { + fullScreen.onStateChange.invoke(false) + } + }) + } + NavHost( modifier = modifier, navController = navController, diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt index 9cd1d25..d22e117 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt @@ -1,5 +1,6 @@ package top.fatweb.oxygen.toolbox.ui +import androidx.activity.ComponentActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -7,7 +8,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing @@ -24,6 +24,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -34,9 +35,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy @@ -59,6 +62,8 @@ import top.fatweb.oxygen.toolbox.ui.component.SearchButtonPosition import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog import top.fatweb.oxygen.toolbox.ui.theme.GradientColors import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors +import top.fatweb.oxygen.toolbox.ui.util.FullScreen +import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -69,159 +74,186 @@ fun OxygenApp(appState: OxygenAppState) { mutableStateOf(false) } - OxygenBackground { - OxygenGradientBackground( - gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors() - ) { - val destination = appState.currentTopLevelDestination + val context = LocalContext.current + val window = (context as ComponentActivity).window + val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) + var isFullScreen by remember { mutableStateOf(false) } - val snackbarHostState = remember { SnackbarHostState() } + val fullScreen = FullScreen( + enable = isFullScreen, + onStateChange = { + isFullScreen = it + } + ) - val isOffline by appState.isOffline.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + windowInsetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } - val noConnectMessage = stringResource(R.string.core_no_connect) + LaunchedEffect(isFullScreen) { + if (isFullScreen) { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + } else { + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + } + } - var canScroll by remember { mutableStateOf(true) } - val topAppBarScrollBehavior = - if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior() + CompositionLocalProvider(LocalFullScreen provides fullScreen) { + OxygenBackground { + OxygenGradientBackground( + gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors() + ) { + val destination = appState.currentTopLevelDestination - var activeSearch by remember { mutableStateOf(false) } - var searchValue by remember { mutableStateOf("") } - var searchCount by remember { mutableIntStateOf(0) } + val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(activeSearch) { - canScroll = !activeSearch - } + val isOffline by appState.isOffline.collectAsStateWithLifecycle() - LaunchedEffect(destination) { - activeSearch = false - searchValue = "" - if (searchCount == 0) { - searchCount++ - } else { - searchCount = 0 + val noConnectMessage = stringResource(R.string.core_no_connect) + + var canScroll by remember { mutableStateOf(true) } + val topAppBarScrollBehavior = + if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior() + + var activeSearch by remember { mutableStateOf(false) } + var searchValue by remember { mutableStateOf("") } + var searchCount by remember { mutableIntStateOf(0) } + + LaunchedEffect(activeSearch) { + canScroll = !activeSearch } - } - LaunchedEffect(isOffline) { - if (isOffline) { - snackbarHostState.showSnackbar( - message = noConnectMessage, - duration = SnackbarDuration.Indefinite + LaunchedEffect(destination) { + activeSearch = false + searchValue = "" + if (searchCount == 0) { + searchCount++ + } else { + searchCount = 0 + } + } + + LaunchedEffect(isOffline) { + if (isOffline) { + snackbarHostState.showSnackbar( + message = noConnectMessage, + duration = SnackbarDuration.Indefinite + ) + } + } + + if (showSettingsDialog) { + SettingsDialog( + onDismiss = { showSettingsDialog = false }, + onNavigateToLibraries = appState::navigateToLibraries, + onNavigateToAbout = appState::navigateToAbout ) } - } - if (showSettingsDialog) { - SettingsDialog( - onDismiss = { showSettingsDialog = false }, - onNavigateToLibraries = appState::navigateToLibraries, - onNavigateToAbout = appState::navigateToAbout - ) - } - - Scaffold( - modifier = Modifier - .nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection), - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground, - contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0), - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - bottomBar = { - AnimatedVisibility( - visible = appState.shouldShowBottomBar && destination != null - ) { - OxygenBottomBar( - destinations = appState.topLevelDestinations, - currentDestination = appState.currentDestination, - onNavigateToDestination = appState::navigateToTopLevelDestination - ) - } - } - ) { padding -> - Row( - Modifier - .fillMaxSize() - .padding(padding) - .consumeWindowInsets(padding) - .windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal + Scaffold( + modifier = Modifier + .nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection), + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + bottomBar = { + AnimatedVisibility( + visible = appState.shouldShowBottomBar && destination != null + ) { + OxygenBottomBar( + destinations = appState.topLevelDestinations, + currentDestination = appState.currentDestination, + onNavigateToDestination = appState::navigateToTopLevelDestination ) - ) - ) { - AnimatedVisibility( - visible = appState.shouldShowNavRail && destination != null - ) { - OxygenNavRail( - modifier = Modifier.safeDrawingPadding(), - destinations = appState.topLevelDestinations, - currentDestination = appState.currentDestination, - onNavigateToDestination = appState::navigateToTopLevelDestination - ) + } } - - Column( - Modifier.fillMaxSize() + ) { padding -> + Row( + Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal + ) + ) ) { AnimatedVisibility( - visible = destination != null + visible = appState.shouldShowNavRail && destination != null ) { - OxygenTopAppBar( - scrollBehavior = topAppBarScrollBehavior, - title = { - destination?.let { - Text( - text = stringResource(destination.titleTextId), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - }, - navigationIcon = OxygenIcons.Search, - navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description), - actionIcon = OxygenIcons.MoreVert, - actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description), - activeSearch = activeSearch, - searchButtonPosition = SearchButtonPosition.Navigation, - query = searchValue, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent - ), - onNavigationClick = { activeSearch = true }, - onActionClick = { showSettingsDialog = true }, - onQueryChange = { - searchValue = it - }, - onSearch = { - searchCount++ - }, - onCancelSearch = { - searchValue = "" - activeSearch = false - searchCount = 0 - } + OxygenNavRail( + modifier = Modifier.safeDrawingPadding(), + destinations = appState.topLevelDestinations, + currentDestination = appState.currentDestination, + onNavigateToDestination = appState::navigateToTopLevelDestination ) } - OxygenNavHost( - appState = appState, - startDestination = when (appState.launchPageConfig) { - LaunchPageConfig.Tools -> TOOLS_ROUTE - LaunchPageConfig.Star -> STAR_ROUTE - }, - isVertical = appState.shouldShowBottomBar, - searchValue = searchValue, - searchCount = searchCount, - onShowSnackbar = { message, action -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = SnackbarDuration.Short - ) == SnackbarResult.ActionPerformed + Column( + Modifier.fillMaxSize() + ) { + AnimatedVisibility( + visible = destination != null + ) { + OxygenTopAppBar( + scrollBehavior = topAppBarScrollBehavior, + title = { + destination?.let { + Text( + text = stringResource(destination.titleTextId), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + }, + navigationIcon = OxygenIcons.Search, + navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description), + actionIcon = OxygenIcons.MoreVert, + actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description), + activeSearch = activeSearch, + searchButtonPosition = SearchButtonPosition.Navigation, + query = searchValue, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent + ), + onNavigationClick = { activeSearch = true }, + onActionClick = { showSettingsDialog = true }, + onQueryChange = { + searchValue = it + }, + onSearch = { + searchCount++ + }, + onCancelSearch = { + searchValue = "" + activeSearch = false + searchCount = 0 + } + ) } - ) + + OxygenNavHost( + appState = appState, + startDestination = when (appState.launchPageConfig) { + LaunchPageConfig.Tools -> TOOLS_ROUTE + LaunchPageConfig.Star -> STAR_ROUTE + }, + isVertical = appState.shouldShowBottomBar, + searchValue = searchValue, + searchCount = searchCount, + onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = SnackbarDuration.Short + ) == SnackbarResult.ActionPerformed + } + ) + } } } } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalFullScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalFullScreen.kt new file mode 100644 index 0000000..26680c3 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalFullScreen.kt @@ -0,0 +1,10 @@ +package top.fatweb.oxygen.toolbox.ui.util + +import androidx.compose.runtime.compositionLocalOf + +data class FullScreen( + val enable: Boolean = false, + val onStateChange: (Boolean) -> Unit = {} +) + +val LocalFullScreen = compositionLocalOf { FullScreen() } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt index 230cb2b..11f5328 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/view/ToolViewScreen.kt @@ -64,6 +64,7 @@ import top.fatweb.oxygen.toolbox.R import top.fatweb.oxygen.toolbox.icon.OxygenIcons import top.fatweb.oxygen.toolbox.ui.component.Indicator import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar +import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils import top.fatweb.oxygen.toolbox.util.NativeWebApi import top.fatweb.oxygen.toolbox.util.Permissions @@ -98,6 +99,7 @@ internal fun ToolViewScreen( isPreview: Boolean, onBackClick: () -> Unit ) { + val (isFullScreen, onFullScreenStateChange) = LocalFullScreen.current Column( modifier @@ -111,7 +113,9 @@ internal fun ToolViewScreen( TopBar( toolViewUiState = toolViewUiState, isPreview = isPreview, - onBackClick = onBackClick + isFullScreen = isFullScreen, + onBackClick = onBackClick, + onFullScreenChange = onFullScreenStateChange ) Content( toolViewUiState = toolViewUiState, @@ -125,7 +129,9 @@ internal fun ToolViewScreen( private fun TopBar( toolViewUiState: ToolViewUiState, isPreview: Boolean, - onBackClick: () -> Unit + isFullScreen: Boolean, + onBackClick: () -> Unit, + onFullScreenChange: (Boolean) -> Unit ) = OxygenTopAppBar( title = { Text( @@ -143,11 +149,16 @@ private fun TopBar( }, navigationIcon = OxygenIcons.Back, navigationIconContentDescription = stringResource(R.string.core_back), + actionIcon = if (isFullScreen) OxygenIcons.FullScreenExit else OxygenIcons.FullScreen, + actionIconContentDescription = stringResource(if (isFullScreen) R.string.core_exit_full_screen else R.string.core_full_screen), colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent ), - onNavigationClick = onBackClick + onNavigationClick = onBackClick, + onActionClick = { + onFullScreenChange(!isFullScreen) + } ) @Composable diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index dca7b9b..ca52924 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -14,6 +14,8 @@ 搜索 加载中… ⚠️ 无法连接至互联网 + 全屏 + 退出全屏 安装 安装中…… 更新 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39477a5..5189579 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,8 @@ Search Loading… ⚠️ Unable to connect to the internet + Full Screen + Exit Full Screen Install Installing… Upgrade