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