Initialize the basic framework

This commit is contained in:
2024-03-14 17:09:28 +08:00
commit 51261e5be9
90 changed files with 3926 additions and 0 deletions

View 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

View File

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

View File

@@ -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 = {})
}
}

View File

@@ -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 = {}
)
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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 = {}
)
}
}

View File

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

View File

@@ -0,0 +1,3 @@
package top.fatweb.oxygen.toolbox.ui.star
class StarScreen

View File

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

View File

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

View File

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

View 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

View File

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

View 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
)
)
)

View File

@@ -0,0 +1,8 @@
package top.fatweb.oxygen.toolbox.ui.tools
import androidx.compose.runtime.Composable
@Composable
internal fun ToolsScreen() {
}

View File

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

View File

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

View File

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