Initialize the basic framework
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# files for the dex VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Windows thumbnail db
|
||||
.DS_Store
|
||||
|
||||
# IDEA/Android Studio project files, because
|
||||
# the project can be imported from settings.gradle.kts
|
||||
*.iml
|
||||
.idea/*
|
||||
!.idea/copyright
|
||||
# Keep the code styles.
|
||||
!/.idea/codeStyles
|
||||
/.idea/codeStyles/*
|
||||
!/.idea/codeStyles/Project.xml
|
||||
!/.idea/codeStyles/codeStyleConfig.xml
|
||||
|
||||
# Gradle cache
|
||||
.gradle
|
||||
|
||||
# Sandbox stuff
|
||||
_sandbox
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
2
app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/src/main/res/raw/dependencies.json
|
||||
163
app/build.gradle.kts
Normal file
@@ -0,0 +1,163 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesTask
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.protobuf)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "top.fatweb.oxygen.toolbox"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "top.fatweb.oxygen.toolbox"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0.0-SNAPSHOT"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
// Required when setting minSdkVersion to 20 or lower
|
||||
multiDexEnabled = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all {
|
||||
outputs.all {
|
||||
(this as BaseVariantOutputImpl).outputFileName =
|
||||
"${project.name}_${defaultConfig.versionName}-${defaultConfig.versionCode}_${buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.10"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = libs.protobuf.protoc.get().toString()
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().forEach { task ->
|
||||
task.builtins {
|
||||
register("java") {
|
||||
option("lite")
|
||||
}
|
||||
register("kotlin") {
|
||||
option("lite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents.beforeVariants {
|
||||
android.sourceSets.getByName(it.name) {
|
||||
val buildDir = layout.buildDirectory.get().asFile
|
||||
java.srcDir(buildDir.resolve("generated/source/proto/${it.name}/java"))
|
||||
kotlin.srcDir(buildDir.resolve("generated/source/proto/${it.name}/kotlin"))
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
registerAndroidTasks = false
|
||||
configPath = "libConfig"
|
||||
outputFileName = "dependencies.json"
|
||||
exclusionPatterns = listOf(
|
||||
Regex("androidx.*"),
|
||||
Regex("org.jetbrains.*"),
|
||||
Regex("com.google.guava:listenablefuture")
|
||||
).map { it.toPattern() }
|
||||
}
|
||||
|
||||
task("exportLibrariesToJson", AboutLibrariesTask::class) {
|
||||
resultDirectory = project.file("src/main/res/raw/")
|
||||
variant = "release"
|
||||
}.dependsOn("collectDependencies")
|
||||
|
||||
afterEvaluate {
|
||||
tasks.findByName("preBuild")?.dependsOn(tasks.findByName("exportLibrariesToJson"))
|
||||
tasks.findByName("kspDebugKotlin")?.dependsOn(tasks.findByName("generateDebugProto"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.hilt.android.testing)
|
||||
androidTestImplementation(libs.androidx.navigation.testing)
|
||||
androidTestImplementation(libs.lifecycle.runtime.testing)
|
||||
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.material.icons.core)
|
||||
implementation(libs.material.icons.extended)
|
||||
implementation(libs.material3.window.size)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.coil.kt)
|
||||
implementation(libs.coil.kt.compose)
|
||||
implementation(libs.coil.kt.svg)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.androidx.dataStore.core)
|
||||
implementation(libs.protobuf.kotlin.lite)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("top.fatweb.oxygen.toolbox", appContext.packageName)
|
||||
}
|
||||
}
|
||||
33
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".OxygenApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Oxygen.Splash"
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="q" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
203
app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivity.kt
Normal file
@@ -0,0 +1,203 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.monitor.NetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import top.fatweb.oxygen.toolbox.ui.OxygenApp
|
||||
import top.fatweb.oxygen.toolbox.ui.rememberOxygenAppState
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import top.fatweb.oxygen.toolbox.ui.util.LocalTimeZone
|
||||
import top.fatweb.oxygen.toolbox.ui.util.LocaleUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TAG = "MainActivity"
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||
|
||||
private val viewModel: MainActivityViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
var uiState: MainActivityUiState by mutableStateOf(MainActivityUiState.Loading)
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiState
|
||||
.onEach { uiState = it }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
when (uiState) {
|
||||
MainActivityUiState.Loading -> true
|
||||
is MainActivityUiState.Success -> false
|
||||
}
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
val locale = whatLocale(uiState)
|
||||
if (uiState != MainActivityUiState.Loading) {
|
||||
LaunchedEffect(locale) {
|
||||
LocaleUtils.switchLocale(this@MainActivity, locale)
|
||||
}
|
||||
}
|
||||
|
||||
val darkTheme = shouldUseDarkTheme(uiState)
|
||||
LaunchedEffect(darkTheme) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
android.graphics.Color.TRANSPARENT
|
||||
) { darkTheme },
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
lightScrim, darkScrim
|
||||
) { darkTheme }
|
||||
)
|
||||
}
|
||||
|
||||
val appState = rememberOxygenAppState(
|
||||
windowSizeClass = calculateWindowSizeClass(this),
|
||||
networkMonitor = networkMonitor,
|
||||
timeZoneMonitor = timeZoneMonitor,
|
||||
launchPageConfig = whatLaunchPage(uiState)
|
||||
)
|
||||
|
||||
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTimeZone provides currentTimeZone
|
||||
) {
|
||||
OxygenTheme(
|
||||
darkTheme = darkTheme,
|
||||
androidTheme = shouldUseAndroidTheme(uiState),
|
||||
dynamicColor = shouldUseDynamicColor(uiState)
|
||||
) {
|
||||
OxygenApp(appState)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "onCreate: C")
|
||||
}
|
||||
|
||||
Log.d(TAG, "onCreate: D")
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface UserDataRepositoryEntryPoint {
|
||||
val userDataRepository: UserDataRepository
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val userDataRepository =
|
||||
EntryPointAccessors.fromApplication<UserDataRepositoryEntryPoint>(newBase).userDataRepository
|
||||
super.attachBaseContext(LocaleUtils.attachBaseContext(newBase, runBlocking {
|
||||
userDataRepository.userData.first().languageConfig
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseDarkTheme(
|
||||
uiState: MainActivityUiState
|
||||
): Boolean = when (uiState) {
|
||||
MainActivityUiState.Loading -> isSystemInDarkTheme()
|
||||
is MainActivityUiState.Success -> when (uiState.userData.darkThemeConfig) {
|
||||
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
||||
DarkThemeConfig.LIGHT -> false
|
||||
DarkThemeConfig.DARK -> true
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseAndroidTheme(
|
||||
uiState: MainActivityUiState
|
||||
): Boolean = when (uiState) {
|
||||
MainActivityUiState.Loading -> false
|
||||
is MainActivityUiState.Success -> when (uiState.userData.themeBrandConfig) {
|
||||
ThemeBrandConfig.DEFAULT -> false
|
||||
ThemeBrandConfig.ANDROID -> true
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseDynamicColor(
|
||||
uiState: MainActivityUiState
|
||||
): Boolean = when (uiState) {
|
||||
MainActivityUiState.Loading -> true
|
||||
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun whatLocale(
|
||||
uiState: MainActivityUiState
|
||||
): LanguageConfig = when (uiState) {
|
||||
MainActivityUiState.Loading -> LanguageConfig.FOLLOW_SYSTEM
|
||||
is MainActivityUiState.Success -> uiState.userData.languageConfig
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun whatLaunchPage(
|
||||
uiState: MainActivityUiState
|
||||
): LaunchPageConfig = when (uiState) {
|
||||
MainActivityUiState.Loading -> LaunchPageConfig.TOOLS
|
||||
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* The default light scrim, as defined by androidx and the platform:
|
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
||||
*/
|
||||
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
||||
|
||||
/**
|
||||
* The default dark scrim, as defined by androidx and the platform:
|
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
||||
*/
|
||||
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
||||
@@ -0,0 +1,31 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
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 top.fatweb.oxygen.toolbox.model.UserData
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
userDataRepository: UserDataRepository
|
||||
) : ViewModel() {
|
||||
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
|
||||
MainActivityUiState.Success(it)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = MainActivityUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface MainActivityUiState {
|
||||
data object Loading : MainActivityUiState
|
||||
data class Success(val userData: UserData) : MainActivityUiState
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class OxygenApplication : Application() {
|
||||
@Inject
|
||||
lateinit var userDataRepository: UserDataRepository
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox.datastore
|
||||
|
||||
import androidx.datastore.core.DataMigration
|
||||
|
||||
internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
|
||||
override suspend fun cleanUp() = Unit
|
||||
|
||||
override suspend fun migrate(currentData: UserPreferences): UserPreferences =
|
||||
currentData.copy { }
|
||||
|
||||
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package top.fatweb.oxygen.toolbox.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.map
|
||||
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.model.UserData
|
||||
import javax.inject.Inject
|
||||
|
||||
class OxygenPreferencesDataSource @Inject constructor(
|
||||
private val userPreferences: DataStore<UserPreferences>
|
||||
) {
|
||||
val userData = userPreferences.data
|
||||
.map {
|
||||
UserData(
|
||||
languageConfig = when (it.languageConfig) {
|
||||
null,
|
||||
LanguageConfigProto.UNRECOGNIZED,
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED,
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
||||
-> LanguageConfig.FOLLOW_SYSTEM
|
||||
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
||||
-> LanguageConfig.CHINESE
|
||||
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
||||
-> LanguageConfig.ENGLISH
|
||||
},
|
||||
launchPageConfig = when (it.launchPageConfig) {
|
||||
null,
|
||||
LaunchPageConfigProto.UNRECOGNIZED,
|
||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_UNSPECIFIED,
|
||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
||||
-> LaunchPageConfig.TOOLS
|
||||
|
||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
||||
-> LaunchPageConfig.STAR
|
||||
},
|
||||
themeBrandConfig = when (it.themeBrandConfig) {
|
||||
null,
|
||||
ThemeBrandConfigProto.UNRECOGNIZED,
|
||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_UNSPECIFIED,
|
||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
||||
->
|
||||
ThemeBrandConfig.DEFAULT
|
||||
|
||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
||||
-> ThemeBrandConfig.ANDROID
|
||||
},
|
||||
darkThemeConfig = when (it.darkThemeConfig) {
|
||||
null,
|
||||
DarkThemeConfigProto.UNRECOGNIZED,
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
||||
->
|
||||
DarkThemeConfig.FOLLOW_SYSTEM
|
||||
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
||||
-> DarkThemeConfig.LIGHT
|
||||
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
||||
-> DarkThemeConfig.DARK
|
||||
},
|
||||
useDynamicColor = it.useDynamicColor
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setLanguageConfig(languageConfig: LanguageConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.languageConfig = when (languageConfig) {
|
||||
LanguageConfig.FOLLOW_SYSTEM -> LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
||||
LanguageConfig.CHINESE -> LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
||||
LanguageConfig.ENGLISH -> LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setLaunchPageConfig(launchPageConfig: LaunchPageConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.launchPageConfig = when (launchPageConfig) {
|
||||
LaunchPageConfig.TOOLS -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
||||
LaunchPageConfig.STAR -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setThemeBrandConfig(themeBrandConfig: ThemeBrandConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.themeBrandConfig = when (themeBrandConfig) {
|
||||
ThemeBrandConfig.DEFAULT -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
||||
ThemeBrandConfig.ANDROID -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.darkThemeConfig = when (darkThemeConfig) {
|
||||
DarkThemeConfig.FOLLOW_SYSTEM -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
||||
DarkThemeConfig.LIGHT -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
||||
DarkThemeConfig.DARK -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.useDynamicColor = useDynamicColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package top.fatweb.oxygen.toolbox.datastore
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserPreferencesSerializer @Inject constructor() : Serializer<UserPreferences> {
|
||||
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): UserPreferences =
|
||||
try {
|
||||
UserPreferences.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
|
||||
t.writeTo(output)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CoroutineScopesModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
@ApplicationScope
|
||||
fun providesCoroutineScope(
|
||||
@Dispatcher(OxygenDispatchers.Default) dispatcher: CoroutineDispatcher
|
||||
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import top.fatweb.oxygen.toolbox.monitor.ConnectivityManagerNetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||
import top.fatweb.oxygen.toolbox.repository.OfflineFirstUserDataRepository
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class DataModule {
|
||||
@Binds
|
||||
internal abstract fun bindsUserDataRepository(userDataRepository: OfflineFirstUserDataRepository): UserDataRepository
|
||||
|
||||
@Binds
|
||||
internal abstract fun bindsNetworkMonitor(networkMonitor: ConnectivityManagerNetworkMonitor): NetworkMonitor
|
||||
|
||||
@Binds
|
||||
internal abstract fun bindsTimeZoneMonitor(timeZoneMonitor: TimeZoneBroadcastMonitor): TimeZoneMonitor
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import top.fatweb.oxygen.toolbox.datastore.IntToStringIdsMigration
|
||||
import top.fatweb.oxygen.toolbox.datastore.UserPreferences
|
||||
import top.fatweb.oxygen.toolbox.datastore.UserPreferencesSerializer
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DataStoreModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun providesUserPreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@Dispatcher(OxygenDispatchers.IO) ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope scope: CoroutineScope,
|
||||
userPreferencesSerializer: UserPreferencesSerializer
|
||||
): DataStore<UserPreferences> =
|
||||
DataStoreFactory.create(
|
||||
serializer = userPreferencesSerializer,
|
||||
scope = CoroutineScope(scope.coroutineContext + ioDispatcher),
|
||||
migrations = listOf(
|
||||
IntToStringIdsMigration
|
||||
)
|
||||
) {
|
||||
context.dataStoreFile("user_preferences.pb")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DispatchersModule {
|
||||
@Provides
|
||||
@Dispatcher(OxygenDispatchers.IO)
|
||||
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@Provides
|
||||
@Dispatcher(OxygenDispatchers.Default)
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package top.fatweb.oxygen.toolbox.icon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
|
||||
object OxygenIcons {
|
||||
val Home = Icons.Rounded.Home
|
||||
val HomeBorder = Icons.Outlined.Home
|
||||
val Star = Icons.Rounded.Star
|
||||
val StarBorder = Icons.Outlined.StarBorder
|
||||
val Search = Icons.Rounded.Search
|
||||
val MoreVert = Icons.Default.MoreVert
|
||||
val Back = Icons.Rounded.ArrowBackIosNew
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class DarkThemeConfig {
|
||||
FOLLOW_SYSTEM,
|
||||
LIGHT,
|
||||
DARK,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class LanguageConfig(val code: String? = null) {
|
||||
FOLLOW_SYSTEM,
|
||||
CHINESE("cn"),
|
||||
ENGLISH("en")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class LaunchPageConfig {
|
||||
TOOLS,
|
||||
STAR
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class ThemeBrandConfig {
|
||||
DEFAULT,
|
||||
ANDROID
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
data class UserData(
|
||||
val languageConfig: LanguageConfig,
|
||||
val launchPageConfig: LaunchPageConfig,
|
||||
val themeBrandConfig: ThemeBrandConfig,
|
||||
val darkThemeConfig: DarkThemeConfig,
|
||||
val useDynamicColor: Boolean
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.core.content.getSystemService
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ConnectivityManagerNetworkMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : NetworkMonitor {
|
||||
override val isOnline: Flow<Boolean> = callbackFlow {
|
||||
val connectivityManager = context.getSystemService<ConnectivityManager>()
|
||||
if (connectivityManager == null) {
|
||||
channel.trySend(false)
|
||||
channel.close()
|
||||
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
val callback = object : NetworkCallback() {
|
||||
private val networks = mutableSetOf<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
networks += network
|
||||
channel.trySend(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
networks -= network
|
||||
channel.trySend(networks.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
val request =
|
||||
NetworkRequest
|
||||
.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
|
||||
channel.trySend(connectivityManager.isCurrentlyConnected())
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
}
|
||||
.conflate()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun ConnectivityManager.isCurrentlyConnected() = when {
|
||||
VERSION.SDK_INT >= VERSION_CODES.M ->
|
||||
activeNetwork
|
||||
?.let(::getNetworkCapabilities)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
|
||||
else -> activeNetworkInfo?.isConnected
|
||||
} ?: false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
val isOnline: Flow<Boolean>
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toKotlinTimeZone
|
||||
import top.fatweb.oxygen.toolbox.di.ApplicationScope
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
import java.time.ZoneId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Singleton
|
||||
class TimeZoneBroadcastMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@ApplicationScope appScope: CoroutineScope,
|
||||
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
|
||||
) : TimeZoneMonitor {
|
||||
override val currentTimeZone: SharedFlow<TimeZone> = callbackFlow {
|
||||
trySend(TimeZone.currentSystemDefault())
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_TIMEZONE_CHANGED) return
|
||||
|
||||
val zonIdFromIntent = if (Build.VERSION.SDK_INT < VERSION_CODES.R) {
|
||||
null
|
||||
} else {
|
||||
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.let { timeZoneId ->
|
||||
val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)
|
||||
zoneId.toKotlinTimeZone()
|
||||
}
|
||||
}
|
||||
|
||||
trySend(zonIdFromIntent ?: TimeZone.currentSystemDefault())
|
||||
}
|
||||
}
|
||||
|
||||
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
|
||||
|
||||
trySend(TimeZone.currentSystemDefault())
|
||||
|
||||
awaitClose {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.conflate()
|
||||
.flowOn(ioDispatcher)
|
||||
.shareIn(appScope, SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), 1)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
||||
interface TimeZoneMonitor {
|
||||
val currentTimeZone: Flow<TimeZone>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import top.fatweb.oxygen.toolbox.ui.OxygenAppState
|
||||
|
||||
@Composable
|
||||
fun OxygenNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
appState: OxygenAppState,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||
startDestination: String
|
||||
) {
|
||||
val navController = appState.navController
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
searchScreen(
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
toolsScreen(
|
||||
|
||||
)
|
||||
starScreen(
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import top.fatweb.oxygen.toolbox.ui.search.SearchRoute
|
||||
|
||||
const val SEARCH_ROUTE = "search_route"
|
||||
|
||||
fun NavController.navigateToSearch(navOptions: NavOptions? = null) =
|
||||
navigate(SEARCH_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.searchScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
composable(
|
||||
route = SEARCH_ROUTE
|
||||
) {
|
||||
SearchRoute(onBackClick = onBackClick)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
const val STAR_ROUTE = "star_route"
|
||||
|
||||
fun NavController.navigateToStar(navOptions: NavOptions) = navigate(STAR_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.starScreen() {
|
||||
composable(
|
||||
route = STAR_ROUTE
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
const val TOOLS_ROUTE = "tools_route"
|
||||
|
||||
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.toolsScreen() {
|
||||
composable(
|
||||
route = TOOLS_ROUTE
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
|
||||
enum class TopLevelDestination(
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector,
|
||||
val iconTextId: Int,
|
||||
val titleTextId: Int
|
||||
) {
|
||||
TOOLS(
|
||||
selectedIcon = OxygenIcons.Home,
|
||||
unselectedIcon = OxygenIcons.HomeBorder,
|
||||
iconTextId = R.string.feature_tools_title,
|
||||
titleTextId = R.string.feature_tools_title
|
||||
),
|
||||
|
||||
STAR(
|
||||
selectedIcon = OxygenIcons.Star,
|
||||
unselectedIcon = OxygenIcons.StarBorder,
|
||||
iconTextId = R.string.feature_star_title,
|
||||
titleTextId = R.string.feature_star_title
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox.network
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Dispatcher(val oxygenDispatcher: OxygenDispatchers)
|
||||
|
||||
enum class OxygenDispatchers {
|
||||
Default,
|
||||
IO
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package top.fatweb.oxygen.toolbox.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import top.fatweb.oxygen.toolbox.datastore.OxygenPreferencesDataSource
|
||||
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.model.UserData
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class OfflineFirstUserDataRepository @Inject constructor(
|
||||
private val oxygenPreferencesDataSource: OxygenPreferencesDataSource
|
||||
) : UserDataRepository {
|
||||
override val userData: Flow<UserData> =
|
||||
oxygenPreferencesDataSource.userData
|
||||
|
||||
override suspend fun setLanguageConfig(languageConfig: LanguageConfig) {
|
||||
oxygenPreferencesDataSource.setLanguageConfig(languageConfig)
|
||||
}
|
||||
|
||||
override suspend fun setLaunchPageConfig(launchPageConfig: LaunchPageConfig) {
|
||||
oxygenPreferencesDataSource.setLaunchPageConfig(launchPageConfig)
|
||||
}
|
||||
|
||||
override suspend fun setThemeBrandConfig(themeBrandConfig: ThemeBrandConfig) {
|
||||
oxygenPreferencesDataSource.setThemeBrandConfig(themeBrandConfig)
|
||||
}
|
||||
|
||||
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
oxygenPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
|
||||
}
|
||||
|
||||
override suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
|
||||
oxygenPreferencesDataSource.setUseDynamicColor(useDynamicColor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package top.fatweb.oxygen.toolbox.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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.model.UserData
|
||||
|
||||
interface UserDataRepository {
|
||||
val userData: Flow<UserData>
|
||||
|
||||
suspend fun setLanguageConfig(languageConfig: LanguageConfig)
|
||||
|
||||
suspend fun setLaunchPageConfig(launchPageConfig: LaunchPageConfig)
|
||||
|
||||
suspend fun setThemeBrandConfig(themeBrandConfig: ThemeBrandConfig)
|
||||
|
||||
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
|
||||
|
||||
suspend fun setUseDynamicColor(useDynamicColor: Boolean)
|
||||
}
|
||||
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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum DarkThemeConfigProto {
|
||||
DARK_THEME_CONFIG_UNSPECIFIED = 0;
|
||||
DARK_THEME_CONFIG_FOLLOW_SYSTEM = 1;
|
||||
DARK_THEME_CONFIG_LIGHT = 2;
|
||||
DARK_THEME_CONFIG_DARK = 3;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum LanguageConfigProto {
|
||||
LANGUAGE_CONFIG_UNSPECIFIED = 0;
|
||||
LANGUAGE_CONFIG_FOLLOW_SYSTEM = 1;
|
||||
LANGUAGE_CONFIG_CHINESE = 2;
|
||||
LANGUAGE_CONFIG_ENGLISH = 3;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum LaunchPageConfigProto {
|
||||
LAUNCH_PAGE_CONFIG_UNSPECIFIED = 0;
|
||||
LAUNCH_PAGE_CONFIG_TOOLS = 1;
|
||||
LAUNCH_PAGE_CONFIG_STAR = 2;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum ThemeBrandConfigProto {
|
||||
THEME_BRAND_CONFIG_UNSPECIFIED = 0;
|
||||
THEME_BRAND_CONFIG_DEFAULT = 1;
|
||||
THEME_BRAND_CONFIG_ANDROID = 2;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "com/fatweb/oxygen/toolbox/data/language_config.proto";
|
||||
import "com/fatweb/oxygen/toolbox/data/launch_page_config.proto";
|
||||
import "com/fatweb/oxygen/toolbox/data/theme_brand_config.proto";
|
||||
import "com/fatweb/oxygen/toolbox/data/dark_theme_config.proto";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message UserPreferences {
|
||||
LanguageConfigProto language_config = 1;
|
||||
LaunchPageConfigProto launch_page_config = 2;
|
||||
ThemeBrandConfigProto theme_brand_config = 3;
|
||||
DarkThemeConfigProto dark_theme_config = 4;
|
||||
bool use_dynamic_color = 5;
|
||||
}
|
||||
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
175
app/src/main/res/drawable/ic_oxygen.xml
Normal file
@@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
10
app/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="NightAdjusted.Theme.Oxygen" parent="android:Theme.Material.NoActionBar" />
|
||||
|
||||
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
27
app/src/main/res/values-zh/strings.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">OxygenToolbox</string>
|
||||
<string name="no_connect">⚠️ 无法连接至互联网</string>
|
||||
<string name="feature_tools_title">工具</string>
|
||||
<string name="feature_star_title">收藏</string>
|
||||
<string name="feature_settings_title">设置</string>
|
||||
<string name="feature_settings_loading">加载中…</string>
|
||||
<string name="feature_settings_language">语言</string>
|
||||
<string name="feature_settings_language_system_default">系统默认</string>
|
||||
<string name="feature_settings_launch_page">启动页</string>
|
||||
<string name="feature_settings_launch_page_tools">工具</string>
|
||||
<string name="feature_settings_launch_page_star">收藏</string>
|
||||
<string name="feature_settings_theme_brand">主题类型</string>
|
||||
<string name="feature_settings_theme_brand_default">默认</string>
|
||||
<string name="feature_settings_theme_brand_android">Android</string>
|
||||
<string name="feature_settings_dark_mode">深色模式</string>
|
||||
<string name="feature_settings_dark_mode_system_default">系统默认</string>
|
||||
<string name="feature_settings_dark_mode_light">明亮</string>
|
||||
<string name="feature_settings_dark_mode_dark">深色</string>
|
||||
<string name="feature_settings_dynamic_color">动态颜色</string>
|
||||
<string name="feature_settings_dynamic_color_enable">启用</string>
|
||||
<string name="feature_settings_dynamic_color_disable">禁用</string>
|
||||
<string name="feature_settings_top_app_bar_action_icon_description">更多</string>
|
||||
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
|
||||
<string name="feature_settings_dismiss_dialog_button_text">完成</string>
|
||||
</resources>
|
||||
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
28
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<resources>
|
||||
<string name="app_name">OxygenToolbox</string>
|
||||
<string name="no_connect">⚠️ Unable to connect to the internet</string>
|
||||
<string name="feature_tools_title">Tools</string>
|
||||
<string name="feature_star_title">Star</string>
|
||||
<string name="feature_settings_title">Settings</string>
|
||||
<string name="feature_settings_loading">Loading…</string>
|
||||
<string name="feature_settings_language">Language</string>
|
||||
<string name="feature_settings_language_system_default">System Default</string>
|
||||
<string name="feature_settings_language_chinese" translatable="false">中文</string>
|
||||
<string name="feature_settings_language_english" translatable="false">English</string>
|
||||
<string name="feature_settings_launch_page">Launch Page</string>
|
||||
<string name="feature_settings_launch_page_tools">Tools</string>
|
||||
<string name="feature_settings_launch_page_star">Star</string>
|
||||
<string name="feature_settings_theme_brand">Theme Brand</string>
|
||||
<string name="feature_settings_theme_brand_default">Default</string>
|
||||
<string name="feature_settings_theme_brand_android">Android</string>
|
||||
<string name="feature_settings_dark_mode">Dark Mode</string>
|
||||
<string name="feature_settings_dark_mode_system_default">System Default</string>
|
||||
<string name="feature_settings_dark_mode_light">Light</string>
|
||||
<string name="feature_settings_dark_mode_dark">Dark</string>
|
||||
<string name="feature_settings_dynamic_color">Dynamic Color</string>
|
||||
<string name="feature_settings_dynamic_color_enable">Enable</string>
|
||||
<string name="feature_settings_dynamic_color_disable">Disable</string>
|
||||
<string name="feature_settings_top_app_bar_action_icon_description">More</string>
|
||||
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
|
||||
<string name="feature_settings_dismiss_dialog_button_text">OK</string>
|
||||
</resources>
|
||||
17
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="NightAdjusted.Theme.Oxygen" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
|
||||
<style name="Theme.Oxygen" parent="NightAdjusted.Theme.Oxygen" />
|
||||
|
||||
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Oxygen.Splash" parent="NightAdjusted.Theme.Splash">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_oxygen</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Oxygen</item>
|
||||
</style>
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,17 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
9
build.gradle.kts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.aboutlibraries) apply false
|
||||
alias(libs.plugins.hilt) apply false
|
||||
alias(libs.plugins.protobuf) apply false
|
||||
}
|
||||
36
gradle.properties
Normal file
@@ -0,0 +1,36 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
org.gradle.parallel=true
|
||||
|
||||
# Enable caching between builds.
|
||||
org.gradle.caching=true
|
||||
|
||||
# Enable configuration caching between builds.
|
||||
org.gradle.configuration-cache=true
|
||||
# This option is set because of https://github.com/google/play-services-plugins/issues/246
|
||||
# to generate the Configuration Cache regardless of incompatible tasks.
|
||||
# See https://github.com/android/nowinandroid/issues/1022 before using it.
|
||||
org.gradle.configuration-cache.problems=warn
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
75
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,75 @@
|
||||
[versions]
|
||||
agp = "8.3.0"
|
||||
kotlin = "1.9.22"
|
||||
ksp = "1.9.22-1.0.18"
|
||||
aboutlibraries = "11.1.0"
|
||||
protobufPlugin = "0.9.4"
|
||||
|
||||
desugarJdkLibs = "2.0.4"
|
||||
composeBom = "2024.02.02"
|
||||
junit = "4.13.2"
|
||||
coreKtx = "1.12.0"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
activityCompose = "1.8.2"
|
||||
appcompat = "1.6.1"
|
||||
androidxLifecycle = "2.7.0"
|
||||
androidxCoreSplashscreen = "1.0.1"
|
||||
hilt = "2.51"
|
||||
coil = "2.5.0"
|
||||
kotlinxDatetime = "0.5.0"
|
||||
androidxDataStore = "1.0.0"
|
||||
protobuf = "3.25.2"
|
||||
androidxNavigation = "2.7.7"
|
||||
androidxHiltNavigationCompose = "1.2.0"
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
aboutlibraries = {id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries"}
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
|
||||
|
||||
[libraries]
|
||||
desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs"}
|
||||
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
material-icons-core = { group = "androidx.compose.material", name = "material-icons-core"}
|
||||
material-icons-extended = {group = "androidx.compose.material", name = "material-icons-extended"}
|
||||
material3-window-size = {group = "androidx.compose.material3", name = "material3-window-size-class"}
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
||||
lifecycle-runtime-testing = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" }
|
||||
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle"}
|
||||
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" }
|
||||
dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "hilt" }
|
||||
hilt-android = {group = "com.google.dagger", name = "hilt-android", version.ref = "hilt"}
|
||||
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
||||
coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
|
||||
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" }
|
||||
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
|
||||
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
|
||||
androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" }
|
||||
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
#Sat Mar 09 10:57:35 CST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
gradlew
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
24
settings.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "OxygenToolbox"
|
||||
include(":app")
|
||||
|
||||