Initialize the basic framework
This commit is contained in:
236
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt
Normal file
236
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt
Normal file
@@ -0,0 +1,236 @@
|
||||
package top.fatweb.oxygen.toolbox.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.navigation.OxygenNavHost
|
||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenBackground
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenGradientBackground
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBar
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBarItem
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
||||
import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OxygenApp(appState: OxygenAppState) {
|
||||
val shouldShowGradientBackground =
|
||||
appState.currentTopLevelDestination == TopLevelDestination.TOOLS
|
||||
var showSettingsDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
OxygenBackground {
|
||||
OxygenGradientBackground(
|
||||
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
|
||||
) {
|
||||
val destination = appState.currentTopLevelDestination
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
||||
|
||||
val noConnectMessage = stringResource(R.string.no_connect)
|
||||
|
||||
LaunchedEffect(isOffline) {
|
||||
if (isOffline) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = noConnectMessage,
|
||||
duration = SnackbarDuration.Indefinite
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSettingsDialog) {
|
||||
SettingsDialog(
|
||||
onDismiss = { showSettingsDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
bottomBar = {
|
||||
if (appState.shouldShowBottomBar && destination != null) {
|
||||
OxygenBottomBar(
|
||||
destinations = appState.topLevelDestinations,
|
||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
||||
currentDestination = appState.currentDestination
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Horizontal
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (appState.shouldShowNavRail && destination != null) {
|
||||
OxygenNavRail(
|
||||
modifier = Modifier.safeDrawingPadding(),
|
||||
destinations = appState.topLevelDestinations,
|
||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
||||
currentDestination = appState.currentDestination
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize()
|
||||
) {
|
||||
if (destination != null) {
|
||||
OxygenTopAppBar(
|
||||
titleRes = destination.titleTextId,
|
||||
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),
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
onNavigationClick = { appState.navigateToSearch() },
|
||||
onActionClick = { showSettingsDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
OxygenNavHost(
|
||||
appState = appState,
|
||||
onShowSnackbar = { message, action ->
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = action,
|
||||
duration = SnackbarDuration.Short
|
||||
) == SnackbarResult.ActionPerformed
|
||||
},
|
||||
startDestination = when (appState.launchPageConfig) {
|
||||
LaunchPageConfig.TOOLS -> TOOLS_ROUTE
|
||||
LaunchPageConfig.STAR -> STAR_ROUTE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OxygenBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
destinations: List<TopLevelDestination>,
|
||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
||||
currentDestination: NavDestination?
|
||||
) {
|
||||
OxygenNavigationBar(
|
||||
modifier = modifier
|
||||
) {
|
||||
destinations.forEach { destination ->
|
||||
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
||||
OxygenNavigationBarItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = { Text(stringResource(destination.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = destination.unselectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = destination.selectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onNavigateToDestination(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OxygenNavRail(
|
||||
modifier: Modifier = Modifier,
|
||||
destinations: List<TopLevelDestination>,
|
||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
||||
currentDestination: NavDestination?
|
||||
) {
|
||||
OxygenNavigationRail(
|
||||
modifier = modifier
|
||||
) {
|
||||
destinations.forEach { destination ->
|
||||
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
||||
OxygenNavigationRailItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = { Text(stringResource(destination.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = destination.unselectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = destination.selectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onNavigateToDestination(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
|
||||
this?.hierarchy?.any {
|
||||
it.route?.contains(destination.name, true) ?: false
|
||||
} ?: false
|
||||
@@ -0,0 +1,117 @@
|
||||
package top.fatweb.oxygen.toolbox.ui
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.datetime.TimeZone
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun rememberOxygenAppState(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
timeZoneMonitor: TimeZoneMonitor,
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
launchPageConfig: LaunchPageConfig
|
||||
): OxygenAppState = remember(
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
timeZoneMonitor,
|
||||
coroutineScope,
|
||||
navController,
|
||||
launchPageConfig
|
||||
) {
|
||||
OxygenAppState(
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
timeZoneMonitor,
|
||||
coroutineScope,
|
||||
navController,
|
||||
launchPageConfig
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
class OxygenAppState(
|
||||
val windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
timeZoneMonitor: TimeZoneMonitor,
|
||||
coroutineScope: CoroutineScope,
|
||||
val navController: NavHostController,
|
||||
val launchPageConfig: LaunchPageConfig
|
||||
) {
|
||||
val currentDestination: NavDestination?
|
||||
@Composable get() = navController
|
||||
.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
val currentTopLevelDestination: TopLevelDestination?
|
||||
@Composable get() = when (currentDestination?.route) {
|
||||
TOOLS_ROUTE -> TopLevelDestination.TOOLS
|
||||
STAR_ROUTE -> TopLevelDestination.STAR
|
||||
else -> null
|
||||
}
|
||||
|
||||
val shouldShowBottomBar: Boolean
|
||||
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact
|
||||
|
||||
val shouldShowNavRail: Boolean
|
||||
get() = !shouldShowBottomBar
|
||||
|
||||
val isOffline = networkMonitor.isOnline
|
||||
.map(Boolean::not)
|
||||
.stateIn(
|
||||
scope = coroutineScope,
|
||||
initialValue = false,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.entries
|
||||
|
||||
val currentTimeZone = timeZoneMonitor.currentTimeZone
|
||||
.stateIn(
|
||||
scope = coroutineScope,
|
||||
initialValue = TimeZone.currentSystemDefault(),
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
|
||||
val topLevelNavOptions = navOptions {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
when (topLevelDestination) {
|
||||
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
|
||||
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToSearch() = navController.navigateToSearch()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalBackgroundTheme
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import kotlin.math.tan
|
||||
|
||||
@Composable
|
||||
fun OxygenBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val color = LocalBackgroundTheme.current.color
|
||||
val tonalElevation = LocalBackgroundTheme.current.tonalElevation
|
||||
Surface(
|
||||
color = if (color == Color.Unspecified) Color.Transparent else color,
|
||||
tonalElevation = if (tonalElevation == Dp.Unspecified) 0.dp else tonalElevation,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
CompositionLocalProvider(LocalAbsoluteTonalElevation provides 0.dp) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenGradientBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientColors: GradientColors = LocalGradientColors.current,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val currentTopColor by rememberUpdatedState(gradientColors.top)
|
||||
val currentBottomColor by rememberUpdatedState(gradientColors.bottom)
|
||||
Surface(
|
||||
color = if (gradientColors.container == Color.Unspecified) Color.Transparent else gradientColors.container,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.drawWithCache {
|
||||
val offset = size.height * tan(
|
||||
Math
|
||||
.toRadians(11.06)
|
||||
.toFloat()
|
||||
)
|
||||
|
||||
val start = Offset(size.width / 2 + offset / 2, 0f)
|
||||
val end = Offset(size.width / 2 - offset / 2, size.height)
|
||||
|
||||
val topGradient = Brush.linearGradient(
|
||||
0f to if (currentTopColor == Color.Unspecified) Color.Transparent else currentTopColor,
|
||||
0.724f to Color.Transparent,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
|
||||
val bottomGradient = Brush.linearGradient(
|
||||
0.2552f to Color.Transparent,
|
||||
1f to if (currentBottomColor == Color.Unspecified) Color.Transparent else currentBottomColor,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
|
||||
onDrawBehind {
|
||||
drawRect(topGradient)
|
||||
drawRect(bottomGradient)
|
||||
}
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
|
||||
annotation class ThemePreviews
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BackgroundDefault() {
|
||||
OxygenTheme(dynamicColor = false) {
|
||||
OxygenBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BackgroundDynamic() {
|
||||
OxygenTheme(dynamicColor = true) {
|
||||
OxygenBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BackgroundAndroid() {
|
||||
OxygenTheme(androidTheme = true) {
|
||||
OxygenBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun GradientBackgroundDefault() {
|
||||
OxygenTheme(dynamicColor = false) {
|
||||
OxygenGradientBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun GradientBackgroundDynamic() {
|
||||
OxygenTheme(dynamicColor = true) {
|
||||
OxygenGradientBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun GradientBackgroundAndroid() {
|
||||
OxygenTheme(androidTheme = true) {
|
||||
OxygenGradientBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.NavigationRailItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
|
||||
@Composable
|
||||
fun RowScope.OxygenNavigationBarItem(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable () -> Unit,
|
||||
selectedIcon: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
alwaysShowLabel: Boolean = false
|
||||
) {
|
||||
NavigationBarItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = label,
|
||||
icon = if (selected) selectedIcon else icon,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
alwaysShowLabel = alwaysShowLabel,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedIconColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenNavigationBar(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
NavigationBar(
|
||||
modifier = modifier,
|
||||
contentColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
content = content,
|
||||
tonalElevation = 0.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenNavigationRailItem(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable () -> Unit,
|
||||
selectedIcon: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
alwaysShowLabel: Boolean = true
|
||||
) {
|
||||
NavigationRailItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = label,
|
||||
icon = if (selected) selectedIcon else icon,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
alwaysShowLabel = alwaysShowLabel,
|
||||
colors = NavigationRailItemDefaults.colors(
|
||||
selectedIconColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedIconColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenNavigationRail(
|
||||
modifier: Modifier = Modifier,
|
||||
header: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
NavigationRail(
|
||||
modifier = modifier,
|
||||
header = header,
|
||||
contentColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
content = content,
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
}
|
||||
|
||||
object OxygenNavigationDefaults {
|
||||
@Composable
|
||||
fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
|
||||
@Composable
|
||||
fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
@Composable
|
||||
fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun OxygenNavigationBarPreview() {
|
||||
val items = TopLevelDestination.entries
|
||||
|
||||
OxygenTheme {
|
||||
OxygenNavigationBar {
|
||||
items.forEachIndexed { index, item ->
|
||||
OxygenNavigationBarItem(
|
||||
selected = index == 0,
|
||||
label = { Text(stringResource(item.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = item.unselectedIcon,
|
||||
contentDescription = stringResource(item.titleTextId)
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = item.selectedIcon, contentDescription = stringResource(
|
||||
item.titleTextId
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun OxygenNavigationRailPreview() {
|
||||
val items = TopLevelDestination.entries
|
||||
|
||||
OxygenTheme {
|
||||
OxygenNavigationRail {
|
||||
items.forEachIndexed { index, item ->
|
||||
OxygenNavigationRailItem(
|
||||
selected = index == 0,
|
||||
label = { Text(stringResource(item.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = item.unselectedIcon,
|
||||
contentDescription = stringResource(item.titleTextId)
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = item.selectedIcon, contentDescription = stringResource(
|
||||
item.titleTextId
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import android.R as androidR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OxygenTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes titleRes: Int,
|
||||
navigationIcon: ImageVector,
|
||||
navigationIconContentDescription: String,
|
||||
actionIcon: ImageVector,
|
||||
actionIconContentDescription: String,
|
||||
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onActionClick: () -> Unit = {}
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier,
|
||||
title = { Text(stringResource(titleRes)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigationClick) {
|
||||
Icon(
|
||||
imageVector = navigationIcon,
|
||||
contentDescription = navigationIconContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onActionClick) {
|
||||
Icon(
|
||||
imageVector = actionIcon,
|
||||
contentDescription = actionIconContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OxygenTopAppBarPreview() {
|
||||
OxygenTheme {
|
||||
OxygenTopAppBar(
|
||||
titleRes = androidR.string.untitled,
|
||||
navigationIcon = OxygenIcons.Search,
|
||||
navigationIconContentDescription = "Navigation icon",
|
||||
actionIcon = OxygenIcons.MoreVert,
|
||||
actionIconContentDescription = "Action icon"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.search
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
|
||||
@Composable
|
||||
internal fun SearchRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClick: () -> Unit,
|
||||
// searchViewmodel: SearchViewModel = hiltViewModel()
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding()
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(imageVector = OxygenIcons.Back, contentDescription = null)
|
||||
}
|
||||
Text("Search")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor(
|
||||
) : ViewModel()
|
||||
@@ -0,0 +1,301 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.ui.component.ThemePreviews
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.supportsDynamicTheming
|
||||
|
||||
@Composable
|
||||
fun SettingsDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: SettingsViewModel = hiltViewModel()
|
||||
) {
|
||||
val settingsUiState by viewModel.settingsUiState.collectAsStateWithLifecycle()
|
||||
SettingsDialog(
|
||||
modifier = modifier,
|
||||
settingsUiState = settingsUiState,
|
||||
onDismiss = onDismiss,
|
||||
onChangeLanguageConfig = viewModel::updateLanguageConfig,
|
||||
onChangeLaunchPageConfig = viewModel::updateLaunchPageConfig,
|
||||
onchangeThemeBrandConfig = viewModel::updateThemeBrandConfig,
|
||||
onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig,
|
||||
onchangeUseDynamicColor = viewModel::updateUseDynamicColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
settingsUiState: SettingsUiState,
|
||||
onDismiss: () -> Unit,
|
||||
supportDynamicColor: Boolean = supportsDynamicTheming(),
|
||||
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
||||
onChangeLaunchPageConfig: (launchPageConfig: LaunchPageConfig) -> Unit,
|
||||
onchangeThemeBrandConfig: (themeBrandConfig: ThemeBrandConfig) -> Unit,
|
||||
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
|
||||
onchangeUseDynamicColor: (useDynamicColor: Boolean) -> Unit
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
AlertDialog(
|
||||
modifier = modifier
|
||||
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
|
||||
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.feature_settings_title),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
text = {
|
||||
HorizontalDivider()
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (settingsUiState) {
|
||||
SettingsUiState.Loading -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
text = stringResource(R.string.feature_settings_loading)
|
||||
)
|
||||
}
|
||||
|
||||
is SettingsUiState.Success -> {
|
||||
SettingsPanel(
|
||||
settings = settingsUiState.settings,
|
||||
supportDynamicColor = supportDynamicColor,
|
||||
onChangeLanguageConfig = onChangeLanguageConfig,
|
||||
onChangeLaunchPageConfig = onChangeLaunchPageConfig,
|
||||
onchangeThemeBrandConfig = onchangeThemeBrandConfig,
|
||||
onChangeDarkThemeConfig = onChangeDarkThemeConfig,
|
||||
onchangeUseDynamicColor = onchangeUseDynamicColor
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable { onDismiss() },
|
||||
text = stringResource(R.string.feature_settings_dismiss_dialog_button_text),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SettingsPanel(
|
||||
settings: UserEditableSettings,
|
||||
supportDynamicColor: Boolean,
|
||||
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
||||
onChangeLaunchPageConfig: (launchPageConfig: LaunchPageConfig) -> Unit,
|
||||
onchangeThemeBrandConfig: (themeBrandConfig: ThemeBrandConfig) -> Unit,
|
||||
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
|
||||
onchangeUseDynamicColor: (useDynamicColor: Boolean) -> Unit
|
||||
) {
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_language))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_language_system_default),
|
||||
selected = settings.languageConfig == LanguageConfig.FOLLOW_SYSTEM,
|
||||
onClick = { onChangeLanguageConfig(LanguageConfig.FOLLOW_SYSTEM) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_language_chinese),
|
||||
selected = settings.languageConfig == LanguageConfig.CHINESE,
|
||||
onClick = { onChangeLanguageConfig(LanguageConfig.CHINESE) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_language_english),
|
||||
selected = settings.languageConfig == LanguageConfig.ENGLISH,
|
||||
onClick = { onChangeLanguageConfig(LanguageConfig.ENGLISH) }
|
||||
)
|
||||
}
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_launch_page))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_launch_page_tools),
|
||||
selected = settings.launchPageConfig == LaunchPageConfig.TOOLS,
|
||||
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.TOOLS) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_launch_page_star),
|
||||
selected = settings.launchPageConfig == LaunchPageConfig.STAR,
|
||||
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.STAR) }
|
||||
)
|
||||
}
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_theme_brand))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_theme_brand_default),
|
||||
selected = settings.themeBrandConfig == ThemeBrandConfig.DEFAULT,
|
||||
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.DEFAULT) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_theme_brand_android),
|
||||
selected = settings.themeBrandConfig == ThemeBrandConfig.ANDROID,
|
||||
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.ANDROID) }
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = settings.themeBrandConfig == ThemeBrandConfig.DEFAULT && supportDynamicColor) {
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_dynamic_color))
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dynamic_color_enable),
|
||||
selected = settings.useDynamicColor,
|
||||
onClick = { onchangeUseDynamicColor(true) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dynamic_color_disable),
|
||||
selected = !settings.useDynamicColor,
|
||||
onClick = { onchangeUseDynamicColor(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_dark_mode))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dark_mode_system_default),
|
||||
selected = settings.darkThemeConfig == DarkThemeConfig.FOLLOW_SYSTEM,
|
||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.FOLLOW_SYSTEM) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dark_mode_light),
|
||||
selected = settings.darkThemeConfig == DarkThemeConfig.LIGHT,
|
||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.LIGHT) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dark_mode_dark),
|
||||
selected = settings.darkThemeConfig == DarkThemeConfig.DARK,
|
||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.DARK) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDialogSectionTitle(text: String) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDialogThemeChooserRow(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.selectable(
|
||||
selected = selected,
|
||||
role = Role.RadioButton,
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun SettingsDialogLoadingPreview() {
|
||||
OxygenTheme {
|
||||
SettingsDialog(
|
||||
onDismiss = { },
|
||||
settingsUiState = SettingsUiState.Loading,
|
||||
onChangeLanguageConfig = {},
|
||||
onChangeLaunchPageConfig = {},
|
||||
onchangeThemeBrandConfig = {},
|
||||
onChangeDarkThemeConfig = {},
|
||||
onchangeUseDynamicColor = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun SettingDialogPreview() {
|
||||
OxygenTheme {
|
||||
SettingsDialog(
|
||||
onDismiss = {},
|
||||
settingsUiState = SettingsUiState.Success(
|
||||
UserEditableSettings(
|
||||
languageConfig = LanguageConfig.FOLLOW_SYSTEM,
|
||||
launchPageConfig = LaunchPageConfig.TOOLS,
|
||||
themeBrandConfig = ThemeBrandConfig.DEFAULT,
|
||||
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
|
||||
useDynamicColor = true
|
||||
)
|
||||
),
|
||||
onChangeLanguageConfig = {},
|
||||
onChangeLaunchPageConfig = {},
|
||||
onchangeThemeBrandConfig = {},
|
||||
onChangeDarkThemeConfig = {},
|
||||
onchangeUseDynamicColor = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val userDataRepository: UserDataRepository
|
||||
) : ViewModel() {
|
||||
val settingsUiState: StateFlow<SettingsUiState> =
|
||||
userDataRepository.userData
|
||||
.map { userData ->
|
||||
SettingsUiState.Success(
|
||||
settings = UserEditableSettings(
|
||||
languageConfig = userData.languageConfig,
|
||||
launchPageConfig = userData.launchPageConfig,
|
||||
themeBrandConfig = userData.themeBrandConfig,
|
||||
darkThemeConfig = userData.darkThemeConfig,
|
||||
useDynamicColor = userData.useDynamicColor
|
||||
)
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = SettingsUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
fun updateLanguageConfig(languageConfig: LanguageConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setLanguageConfig(languageConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLaunchPageConfig(launchPageConfig: LaunchPageConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setLaunchPageConfig(launchPageConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateThemeBrandConfig(themeBrandConfig: ThemeBrandConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setThemeBrandConfig(themeBrandConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setDarkThemeConfig(darkThemeConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUseDynamicColor(useDynamicColor: Boolean) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setUseDynamicColor(useDynamicColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UserEditableSettings(
|
||||
val languageConfig: LanguageConfig,
|
||||
val launchPageConfig: LaunchPageConfig,
|
||||
val themeBrandConfig: ThemeBrandConfig,
|
||||
val darkThemeConfig: DarkThemeConfig,
|
||||
val useDynamicColor: Boolean
|
||||
)
|
||||
|
||||
sealed interface SettingsUiState {
|
||||
data object Loading : SettingsUiState
|
||||
data class Success(val settings: UserEditableSettings) : SettingsUiState
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.star
|
||||
|
||||
class StarScreen
|
||||
@@ -0,0 +1,14 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Immutable
|
||||
data class BackgroundTheme(
|
||||
val color: Color = Color.Unspecified,
|
||||
val tonalElevation: Dp = Dp.Unspecified
|
||||
)
|
||||
|
||||
val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() }
|
||||
@@ -0,0 +1,66 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal val Blue10 = Color(0xFF001F28)
|
||||
internal val Blue20 = Color(0xFF003544)
|
||||
internal val Blue30 = Color(0xFF004D61)
|
||||
internal val Blue40 = Color(0xFF006780)
|
||||
internal val Blue80 = Color(0xFF5DD5FC)
|
||||
internal val Blue90 = Color(0xFFB8EAFF)
|
||||
internal val DarkGreen10 = Color(0xFF0D1F12)
|
||||
internal val DarkGreen20 = Color(0xFF223526)
|
||||
internal val DarkGreen30 = Color(0xFF394B3C)
|
||||
internal val DarkGreen40 = Color(0xFF4F6352)
|
||||
internal val DarkGreen80 = Color(0xFFB7CCB8)
|
||||
internal val DarkGreen90 = Color(0xFFD3E8D3)
|
||||
internal val DarkGreenGray10 = Color(0xFF1A1C1A)
|
||||
internal val DarkGreenGray20 = Color(0xFF2F312E)
|
||||
internal val DarkGreenGray90 = Color(0xFFE2E3DE)
|
||||
internal val DarkGreenGray95 = Color(0xFFF0F1EC)
|
||||
internal val DarkGreenGray99 = Color(0xFFFBFDF7)
|
||||
internal val DarkPurpleGray10 = Color(0xFF201A1B)
|
||||
internal val DarkPurpleGray20 = Color(0xFF362F30)
|
||||
internal val DarkPurpleGray90 = Color(0xFFECDFE0)
|
||||
internal val DarkPurpleGray95 = Color(0xFFFAEEEF)
|
||||
internal val DarkPurpleGray99 = Color(0xFFFCFCFC)
|
||||
internal val Green10 = Color(0xFF00210B)
|
||||
internal val Green20 = Color(0xFF003919)
|
||||
internal val Green30 = Color(0xFF005227)
|
||||
internal val Green40 = Color(0xFF006D36)
|
||||
internal val Green80 = Color(0xFF0EE37C)
|
||||
internal val Green90 = Color(0xFF5AFF9D)
|
||||
internal val GreenGray30 = Color(0xFF414941)
|
||||
internal val GreenGray50 = Color(0xFF727971)
|
||||
internal val GreenGray60 = Color(0xFF8B938A)
|
||||
internal val GreenGray80 = Color(0xFFC1C9BF)
|
||||
internal val GreenGray90 = Color(0xFFDDE5DB)
|
||||
internal val Orange10 = Color(0xFF380D00)
|
||||
internal val Orange20 = Color(0xFF5B1A00)
|
||||
internal val Orange30 = Color(0xFF812800)
|
||||
internal val Orange40 = Color(0xFFA23F16)
|
||||
internal val Orange80 = Color(0xFFFFB59B)
|
||||
internal val Orange90 = Color(0xFFFFDBCF)
|
||||
internal val Purple10 = Color(0xFF36003C)
|
||||
internal val Purple20 = Color(0xFF560A5D)
|
||||
internal val Purple30 = Color(0xFF702776)
|
||||
internal val Purple40 = Color(0xFF8B418F)
|
||||
internal val Purple80 = Color(0xFFFFA9FE)
|
||||
internal val Purple90 = Color(0xFFFFD6FA)
|
||||
internal val PurpleGray30 = Color(0xFF4D444C)
|
||||
internal val PurpleGray50 = Color(0xFF7F747C)
|
||||
internal val PurpleGray60 = Color(0xFF998D96)
|
||||
internal val PurpleGray80 = Color(0xFFD0C3CC)
|
||||
internal val PurpleGray90 = Color(0xFFEDDEE8)
|
||||
internal val Red10 = Color(0xFF410002)
|
||||
internal val Red20 = Color(0xFF690005)
|
||||
internal val Red30 = Color(0xFF93000A)
|
||||
internal val Red40 = Color(0xFFBA1A1A)
|
||||
internal val Red80 = Color(0xFFFFB4AB)
|
||||
internal val Red90 = Color(0xFFFFDAD6)
|
||||
internal val Teal10 = Color(0xFF001F26)
|
||||
internal val Teal20 = Color(0xFF02363F)
|
||||
internal val Teal30 = Color(0xFF214D56)
|
||||
internal val Teal40 = Color(0xFF3A656F)
|
||||
internal val Teal80 = Color(0xFFA2CED9)
|
||||
internal val Teal90 = Color(0xFFBEEAF6)
|
||||
@@ -0,0 +1,14 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
data class GradientColors(
|
||||
val top: Color = Color.Unspecified,
|
||||
val bottom: Color = Color.Unspecified,
|
||||
val container: Color = Color.Unspecified
|
||||
)
|
||||
|
||||
val LocalGradientColors = staticCompositionLocalOf { GradientColors() }
|
||||
194
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Theme.kt
Normal file
194
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val LightDefaultColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Purple90,
|
||||
onPrimaryContainer = Purple10,
|
||||
secondary = Orange40,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = Orange90,
|
||||
onSecondaryContainer = Orange10,
|
||||
tertiary = Blue40,
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = Blue90,
|
||||
onTertiaryContainer = Blue10,
|
||||
error = Red40,
|
||||
onError = Color.White,
|
||||
errorContainer = Red90,
|
||||
onErrorContainer = Red10,
|
||||
background = DarkPurpleGray99,
|
||||
onBackground = DarkPurpleGray10,
|
||||
surface = DarkPurpleGray99,
|
||||
onSurface = DarkPurpleGray10,
|
||||
surfaceVariant = PurpleGray90,
|
||||
onSurfaceVariant = PurpleGray30,
|
||||
inverseSurface = DarkPurpleGray20,
|
||||
inverseOnSurface = DarkPurpleGray95,
|
||||
outline = PurpleGray50,
|
||||
)
|
||||
|
||||
val DarkDefaultColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
onPrimary = Purple20,
|
||||
primaryContainer = Purple30,
|
||||
onPrimaryContainer = Purple90,
|
||||
secondary = Orange80,
|
||||
onSecondary = Orange20,
|
||||
secondaryContainer = Orange30,
|
||||
onSecondaryContainer = Orange90,
|
||||
tertiary = Blue80,
|
||||
onTertiary = Blue20,
|
||||
tertiaryContainer = Blue30,
|
||||
onTertiaryContainer = Blue90,
|
||||
error = Red80,
|
||||
onError = Red20,
|
||||
errorContainer = Red30,
|
||||
onErrorContainer = Red90,
|
||||
background = DarkPurpleGray10,
|
||||
onBackground = DarkPurpleGray90,
|
||||
surface = DarkPurpleGray10,
|
||||
onSurface = DarkPurpleGray90,
|
||||
surfaceVariant = PurpleGray30,
|
||||
onSurfaceVariant = PurpleGray80,
|
||||
inverseSurface = DarkPurpleGray90,
|
||||
inverseOnSurface = DarkPurpleGray10,
|
||||
outline = PurpleGray60,
|
||||
)
|
||||
|
||||
val LightAndroidColorScheme = lightColorScheme(
|
||||
primary = Green40,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Green90,
|
||||
onPrimaryContainer = Green10,
|
||||
secondary = DarkGreen40,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = DarkGreen90,
|
||||
onSecondaryContainer = DarkGreen10,
|
||||
tertiary = Teal40,
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = Teal90,
|
||||
onTertiaryContainer = Teal10,
|
||||
error = Red40,
|
||||
onError = Color.White,
|
||||
errorContainer = Red90,
|
||||
onErrorContainer = Red10,
|
||||
background = DarkGreenGray99,
|
||||
onBackground = DarkGreenGray10,
|
||||
surface = DarkGreenGray99,
|
||||
onSurface = DarkGreenGray10,
|
||||
surfaceVariant = GreenGray90,
|
||||
onSurfaceVariant = GreenGray30,
|
||||
inverseSurface = DarkGreenGray20,
|
||||
inverseOnSurface = DarkGreenGray95,
|
||||
outline = GreenGray50,
|
||||
)
|
||||
|
||||
val DarkAndroidColorScheme = darkColorScheme(
|
||||
primary = Green80,
|
||||
onPrimary = Green20,
|
||||
primaryContainer = Green30,
|
||||
onPrimaryContainer = Green90,
|
||||
secondary = DarkGreen80,
|
||||
onSecondary = DarkGreen20,
|
||||
secondaryContainer = DarkGreen30,
|
||||
onSecondaryContainer = DarkGreen90,
|
||||
tertiary = Teal80,
|
||||
onTertiary = Teal20,
|
||||
tertiaryContainer = Teal30,
|
||||
onTertiaryContainer = Teal90,
|
||||
error = Red80,
|
||||
onError = Red20,
|
||||
errorContainer = Red30,
|
||||
onErrorContainer = Red90,
|
||||
background = DarkGreenGray10,
|
||||
onBackground = DarkGreenGray90,
|
||||
surface = DarkGreenGray10,
|
||||
onSurface = DarkGreenGray90,
|
||||
surfaceVariant = GreenGray30,
|
||||
onSurfaceVariant = GreenGray80,
|
||||
inverseSurface = DarkGreenGray90,
|
||||
inverseOnSurface = DarkGreenGray10,
|
||||
outline = GreenGray60,
|
||||
)
|
||||
|
||||
val LightAndroidGradientColors = GradientColors(container = DarkGreenGray95)
|
||||
val DarkAndroidGradientColors = GradientColors(container = Color.Black)
|
||||
val LightAndroidBackgroundTheme = BackgroundTheme(color = DarkGreenGray95)
|
||||
val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black)
|
||||
|
||||
@Composable
|
||||
fun OxygenTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
androidTheme: Boolean = false,
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
|
||||
|
||||
dynamicColor && supportsDynamicTheming() -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
|
||||
}
|
||||
|
||||
val emptyGradientColors = GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
|
||||
val defaultGradientColors = GradientColors(
|
||||
top = colorScheme.inverseOnSurface,
|
||||
bottom = colorScheme.primaryContainer,
|
||||
container = colorScheme.surface
|
||||
)
|
||||
val gradientColors = when {
|
||||
androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors
|
||||
dynamicColor && supportsDynamicTheming() -> emptyGradientColors
|
||||
else -> defaultGradientColors
|
||||
}
|
||||
|
||||
val defaultBackgroundTheme = BackgroundTheme(
|
||||
color = colorScheme.surface,
|
||||
tonalElevation = 2.dp
|
||||
)
|
||||
val backgroundTheme = when {
|
||||
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
|
||||
else -> defaultBackgroundTheme
|
||||
}
|
||||
val tintTheme = when {
|
||||
androidTheme -> TintTheme()
|
||||
dynamicColor && supportsDynamicTheming() -> TintTheme(colorScheme.primary)
|
||||
else -> TintTheme()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalGradientColors provides gradientColors,
|
||||
LocalBackgroundTheme provides backgroundTheme,
|
||||
LocalTintTheme provides tintTheme
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = OxygenTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
|
||||
fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
data class TintTheme(
|
||||
val iconTint: Color = Color.Unspecified
|
||||
)
|
||||
|
||||
val LocalTintTheme = staticCompositionLocalOf { TintTheme() }
|
||||
129
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Type.kt
Normal file
129
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Type.kt
Normal file
@@ -0,0 +1,129 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
internal val OxygenTypography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Bottom,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Bottom,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
// Default text style
|
||||
bodyLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
// Used for Button
|
||||
labelLarge = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
),
|
||||
// Used for Navigation items
|
||||
labelMedium = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
),
|
||||
// Used for Tag
|
||||
labelSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 10.sp,
|
||||
lineHeight = 14.sp,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
internal fun ToolsScreen() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.util
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
||||
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
|
||||
@@ -0,0 +1,58 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.annotation.RequiresApi
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleUtils {
|
||||
fun switchLocale(activity: Activity, languageConfig: LanguageConfig) {
|
||||
val newLanguage = when (languageConfig) {
|
||||
LanguageConfig.FOLLOW_SYSTEM -> ResourcesUtils.getSystemLocale().get(0)!!.language
|
||||
LanguageConfig.CHINESE -> "zh"
|
||||
LanguageConfig.ENGLISH -> "en"
|
||||
}
|
||||
val currentLanguage = ResourcesUtils.getAppLocale(activity).language
|
||||
if (newLanguage != currentLanguage) {
|
||||
activity.recreate()
|
||||
}
|
||||
}
|
||||
|
||||
fun attachBaseContext(context: Context, languageConfig: LanguageConfig): Context {
|
||||
val locale: Locale = getLocaleFromLanguageConfig(languageConfig)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createConfigurationContext(context, locale)
|
||||
} else {
|
||||
updateConfiguration(context, locale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocaleFromLanguageConfig(languageConfig: LanguageConfig): Locale =
|
||||
when (languageConfig) {
|
||||
LanguageConfig.FOLLOW_SYSTEM -> ResourcesUtils.getSystemLocale().get(0)!!
|
||||
LanguageConfig.CHINESE -> Locale("zh")
|
||||
LanguageConfig.ENGLISH -> Locale("en")
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun createConfigurationContext(context: Context, locale: Locale): Context {
|
||||
val configuration = context.resources.configuration
|
||||
configuration.setLocales(LocaleList(locale))
|
||||
|
||||
return context.createConfigurationContext(configuration)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun updateConfiguration(context: Context, locale: Locale): Context {
|
||||
val resources = context.resources
|
||||
val configuration = resources.configuration
|
||||
configuration.locale = locale
|
||||
resources.updateConfiguration(configuration, resources.displayMetrics)
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.util.Locale
|
||||
|
||||
object ResourcesUtils {
|
||||
fun getConfiguration(context: Context) = context.resources.configuration
|
||||
|
||||
fun getDisplayMetrics(context: Context) = context.resources.displayMetrics
|
||||
|
||||
fun getAppLocale(context: Context): Locale =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) getConfiguration(context).locales.get(0)
|
||||
else getConfiguration(context).locale
|
||||
|
||||
fun getSystemLocale(): LocaleListCompat =
|
||||
ConfigurationCompat.getLocales(Resources.getSystem().configuration)
|
||||
|
||||
fun getAppVersionName(context: Context): String =
|
||||
try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
"Unknown"
|
||||
}
|
||||
|
||||
fun getAppVersionCode(context: Context): Long =
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode
|
||||
else context.packageManager.getPackageInfo(context.packageName, 0).versionCode.toLong()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user