Feat(App): Support full screen

This commit is contained in:
2024-10-14 18:07:03 +08:00
parent 2823788765
commit e855a414a4
7 changed files with 218 additions and 137 deletions

View File

@@ -10,6 +10,8 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download 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.Inbox
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Reorder import androidx.compose.material.icons.filled.Reorder
@@ -43,6 +45,8 @@ object OxygenIcons {
val Delete = Icons.Default.Delete val Delete = Icons.Default.Delete
val Download = Icons.Default.Download val Download = Icons.Default.Download
val Error = Icons.Default.Cancel val Error = Icons.Default.Cancel
val FullScreen = Icons.Default.Fullscreen
val FullScreenExit = Icons.Default.FullscreenExit
val Home = Icons.Rounded.Home val Home = Icons.Rounded.Home
val HomeBorder = Icons.Outlined.Home val HomeBorder = Icons.Outlined.Home
val Info = Icons.Outlined.Info val Info = Icons.Outlined.Info

View File

@@ -1,9 +1,14 @@
package top.fatweb.oxygen.toolbox.navigation package top.fatweb.oxygen.toolbox.navigation
import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import top.fatweb.oxygen.toolbox.ui.OxygenAppState import top.fatweb.oxygen.toolbox.ui.OxygenAppState
import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen
@Composable @Composable
fun OxygenNavHost( fun OxygenNavHost(
@@ -16,6 +21,21 @@ fun OxygenNavHost(
onShowSnackbar: suspend (message: String, action: String?) -> Boolean onShowSnackbar: suspend (message: String, action: String?) -> Boolean
) { ) {
val navController = appState.navController 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( NavHost(
modifier = modifier, modifier = modifier,
navController = navController, navController = navController,

View File

@@ -1,5 +1,6 @@
package top.fatweb.oxygen.toolbox.ui package top.fatweb.oxygen.toolbox.ui
import androidx.activity.ComponentActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
@@ -24,6 +24,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -34,9 +35,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow 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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy 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.settings.SettingsDialog
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -69,159 +74,186 @@ fun OxygenApp(appState: OxygenAppState) {
mutableStateOf(false) mutableStateOf(false)
} }
OxygenBackground { val context = LocalContext.current
OxygenGradientBackground( val window = (context as ComponentActivity).window
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors() val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
) { var isFullScreen by remember { mutableStateOf(false) }
val destination = appState.currentTopLevelDestination
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) } CompositionLocalProvider(LocalFullScreen provides fullScreen) {
val topAppBarScrollBehavior = OxygenBackground {
if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior() OxygenGradientBackground(
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
) {
val destination = appState.currentTopLevelDestination
var activeSearch by remember { mutableStateOf(false) } val snackbarHostState = remember { SnackbarHostState() }
var searchValue by remember { mutableStateOf("") }
var searchCount by remember { mutableIntStateOf(0) }
LaunchedEffect(activeSearch) { val isOffline by appState.isOffline.collectAsStateWithLifecycle()
canScroll = !activeSearch
}
LaunchedEffect(destination) { val noConnectMessage = stringResource(R.string.core_no_connect)
activeSearch = false
searchValue = "" var canScroll by remember { mutableStateOf(true) }
if (searchCount == 0) { val topAppBarScrollBehavior =
searchCount++ if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior()
} else {
searchCount = 0 var activeSearch by remember { mutableStateOf(false) }
var searchValue by remember { mutableStateOf("") }
var searchCount by remember { mutableIntStateOf(0) }
LaunchedEffect(activeSearch) {
canScroll = !activeSearch
} }
}
LaunchedEffect(isOffline) { LaunchedEffect(destination) {
if (isOffline) { activeSearch = false
snackbarHostState.showSnackbar( searchValue = ""
message = noConnectMessage, if (searchCount == 0) {
duration = SnackbarDuration.Indefinite 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) { Scaffold(
SettingsDialog( modifier = Modifier
onDismiss = { showSettingsDialog = false }, .nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
onNavigateToLibraries = appState::navigateToLibraries, containerColor = Color.Transparent,
onNavigateToAbout = appState::navigateToAbout contentColor = MaterialTheme.colorScheme.onBackground,
) contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
} snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
bottomBar = {
Scaffold( AnimatedVisibility(
modifier = Modifier visible = appState.shouldShowBottomBar && destination != null
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection), ) {
containerColor = Color.Transparent, OxygenBottomBar(
contentColor = MaterialTheme.colorScheme.onBackground, destinations = appState.topLevelDestinations,
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0), currentDestination = appState.currentDestination,
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, onNavigateToDestination = appState::navigateToTopLevelDestination
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
) )
) }
) {
AnimatedVisibility(
visible = appState.shouldShowNavRail && destination != null
) {
OxygenNavRail(
modifier = Modifier.safeDrawingPadding(),
destinations = appState.topLevelDestinations,
currentDestination = appState.currentDestination,
onNavigateToDestination = appState::navigateToTopLevelDestination
)
} }
) { padding ->
Column( Row(
Modifier.fillMaxSize() Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
)
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = destination != null visible = appState.shouldShowNavRail && destination != null
) { ) {
OxygenTopAppBar( OxygenNavRail(
scrollBehavior = topAppBarScrollBehavior, modifier = Modifier.safeDrawingPadding(),
title = { destinations = appState.topLevelDestinations,
destination?.let { currentDestination = appState.currentDestination,
Text( onNavigateToDestination = appState::navigateToTopLevelDestination
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( Column(
appState = appState, Modifier.fillMaxSize()
startDestination = when (appState.launchPageConfig) { ) {
LaunchPageConfig.Tools -> TOOLS_ROUTE AnimatedVisibility(
LaunchPageConfig.Star -> STAR_ROUTE visible = destination != null
}, ) {
isVertical = appState.shouldShowBottomBar, OxygenTopAppBar(
searchValue = searchValue, scrollBehavior = topAppBarScrollBehavior,
searchCount = searchCount, title = {
onShowSnackbar = { message, action -> destination?.let {
snackbarHostState.showSnackbar( Text(
message = message, text = stringResource(destination.titleTextId),
actionLabel = action, maxLines = 1,
duration = SnackbarDuration.Short overflow = TextOverflow.Ellipsis
) == SnackbarResult.ActionPerformed )
}
},
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
}
)
}
} }
} }
} }

View File

@@ -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() }

View File

@@ -64,6 +64,7 @@ import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.ui.component.Indicator import top.fatweb.oxygen.toolbox.ui.component.Indicator
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar 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.ui.util.ResourcesUtils
import top.fatweb.oxygen.toolbox.util.NativeWebApi import top.fatweb.oxygen.toolbox.util.NativeWebApi
import top.fatweb.oxygen.toolbox.util.Permissions import top.fatweb.oxygen.toolbox.util.Permissions
@@ -98,6 +99,7 @@ internal fun ToolViewScreen(
isPreview: Boolean, isPreview: Boolean,
onBackClick: () -> Unit onBackClick: () -> Unit
) { ) {
val (isFullScreen, onFullScreenStateChange) = LocalFullScreen.current
Column( Column(
modifier modifier
@@ -111,7 +113,9 @@ internal fun ToolViewScreen(
TopBar( TopBar(
toolViewUiState = toolViewUiState, toolViewUiState = toolViewUiState,
isPreview = isPreview, isPreview = isPreview,
onBackClick = onBackClick isFullScreen = isFullScreen,
onBackClick = onBackClick,
onFullScreenChange = onFullScreenStateChange
) )
Content( Content(
toolViewUiState = toolViewUiState, toolViewUiState = toolViewUiState,
@@ -125,7 +129,9 @@ internal fun ToolViewScreen(
private fun TopBar( private fun TopBar(
toolViewUiState: ToolViewUiState, toolViewUiState: ToolViewUiState,
isPreview: Boolean, isPreview: Boolean,
onBackClick: () -> Unit isFullScreen: Boolean,
onBackClick: () -> Unit,
onFullScreenChange: (Boolean) -> Unit
) = OxygenTopAppBar( ) = OxygenTopAppBar(
title = { title = {
Text( Text(
@@ -143,11 +149,16 @@ private fun TopBar(
}, },
navigationIcon = OxygenIcons.Back, navigationIcon = OxygenIcons.Back,
navigationIconContentDescription = stringResource(R.string.core_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( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent scrolledContainerColor = Color.Transparent
), ),
onNavigationClick = onBackClick onNavigationClick = onBackClick,
onActionClick = {
onFullScreenChange(!isFullScreen)
}
) )
@Composable @Composable

View File

@@ -14,6 +14,8 @@
<string name="core_search">搜索</string> <string name="core_search">搜索</string>
<string name="core_loading">加载中…</string> <string name="core_loading">加载中…</string>
<string name="core_no_connect">⚠️ 无法连接至互联网</string> <string name="core_no_connect">⚠️ 无法连接至互联网</string>
<string name="core_full_screen">全屏</string>
<string name="core_exit_full_screen">退出全屏</string>
<string name="core_install">安装</string> <string name="core_install">安装</string>
<string name="core_installing">安装中……</string> <string name="core_installing">安装中……</string>
<string name="core_upgrade">更新</string> <string name="core_upgrade">更新</string>

View File

@@ -13,6 +13,8 @@
<string name="core_search">Search</string> <string name="core_search">Search</string>
<string name="core_loading">Loading…</string> <string name="core_loading">Loading…</string>
<string name="core_no_connect">⚠️ Unable to connect to the internet</string> <string name="core_no_connect">⚠️ Unable to connect to the internet</string>
<string name="core_full_screen">Full Screen</string>
<string name="core_exit_full_screen">Exit Full Screen</string>
<string name="core_install">Install</string> <string name="core_install">Install</string>
<string name="core_installing">Installing…</string> <string name="core_installing">Installing…</string>
<string name="core_upgrade">Upgrade</string> <string name="core_upgrade">Upgrade</string>