commit 51261e5be9da6eb23d4e780c7eec071b9068215f Author: FatttSnake Date: Thu Mar 14 17:09:28 2024 +0800 Initialize the basic framework diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d448259 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..1d10f0e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +/src/main/res/raw/dependencies.json \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d8eeaa9 --- /dev/null +++ b/app/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/kotlin/top/fatweb/oxygen/toolbox/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/top/fatweb/oxygen/toolbox/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..206f754 --- /dev/null +++ b/app/src/androidTest/kotlin/top/fatweb/oxygen/toolbox/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dfc821f --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivity.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivity.kt new file mode 100644 index 0000000..7e7ac44 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivity.kt @@ -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(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) diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivityViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivityViewModel.kt new file mode 100644 index 0000000..07c49b9 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivityViewModel.kt @@ -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 = 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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/OxygenApplication.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/OxygenApplication.kt new file mode 100644 index 0000000..67e1148 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/OxygenApplication.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/IntToStringIdsMigration.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/IntToStringIdsMigration.kt new file mode 100644 index 0000000..4cc379f --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/IntToStringIdsMigration.kt @@ -0,0 +1,12 @@ +package top.fatweb.oxygen.toolbox.datastore + +import androidx.datastore.core.DataMigration + +internal object IntToStringIdsMigration : DataMigration { + override suspend fun cleanUp() = Unit + + override suspend fun migrate(currentData: UserPreferences): UserPreferences = + currentData.copy { } + + override suspend fun shouldMigrate(currentData: UserPreferences): Boolean = false +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/OxygenPreferencesDataSource.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/OxygenPreferencesDataSource.kt new file mode 100644 index 0000000..e172df5 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/OxygenPreferencesDataSource.kt @@ -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 +) { + 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 + } + } + } +} diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/UserPreferencesSerializer.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/UserPreferencesSerializer.kt new file mode 100644 index 0000000..e387bc5 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/datastore/UserPreferencesSerializer.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/CoroutineScopesModule.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/CoroutineScopesModule.kt new file mode 100644 index 0000000..5a2017d --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/CoroutineScopesModule.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt new file mode 100644 index 0000000..64e0591 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataModule.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataStoreModule.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataStoreModule.kt new file mode 100644 index 0000000..ec1a090 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DataStoreModule.kt @@ -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 = + DataStoreFactory.create( + serializer = userPreferencesSerializer, + scope = CoroutineScope(scope.coroutineContext + ioDispatcher), + migrations = listOf( + IntToStringIdsMigration + ) + ) { + context.dataStoreFile("user_preferences.pb") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DispatchersModule.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DispatchersModule.kt new file mode 100644 index 0000000..4637eb0 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/di/DispatchersModule.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt new file mode 100644 index 0000000..710e20d --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/DarkThemeConfig.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/DarkThemeConfig.kt new file mode 100644 index 0000000..199f7f2 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/DarkThemeConfig.kt @@ -0,0 +1,7 @@ +package top.fatweb.oxygen.toolbox.model + +enum class DarkThemeConfig { + FOLLOW_SYSTEM, + LIGHT, + DARK, +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/LanguageConfig.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/LanguageConfig.kt new file mode 100644 index 0000000..9cfb529 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/LanguageConfig.kt @@ -0,0 +1,7 @@ +package top.fatweb.oxygen.toolbox.model + +enum class LanguageConfig(val code: String? = null) { + FOLLOW_SYSTEM, + CHINESE("cn"), + ENGLISH("en") +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/LaunchPageConfig.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/LaunchPageConfig.kt new file mode 100644 index 0000000..09218cc --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/LaunchPageConfig.kt @@ -0,0 +1,6 @@ +package top.fatweb.oxygen.toolbox.model + +enum class LaunchPageConfig { + TOOLS, + STAR +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/ThemeBrandConfig.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/ThemeBrandConfig.kt new file mode 100644 index 0000000..fa2bc05 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/ThemeBrandConfig.kt @@ -0,0 +1,6 @@ +package top.fatweb.oxygen.toolbox.model + +enum class ThemeBrandConfig { + DEFAULT, + ANDROID +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/UserData.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/UserData.kt new file mode 100644 index 0000000..f790a7e --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/UserData.kt @@ -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 +) diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/ConnectivityManagerNetworkMonitor.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/ConnectivityManagerNetworkMonitor.kt new file mode 100644 index 0000000..6246c08 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/ConnectivityManagerNetworkMonitor.kt @@ -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 = callbackFlow { + val connectivityManager = context.getSystemService() + if (connectivityManager == null) { + channel.trySend(false) + channel.close() + + return@callbackFlow + } + + val callback = object : NetworkCallback() { + private val networks = mutableSetOf() + + 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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/NetworkMonitor.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/NetworkMonitor.kt new file mode 100644 index 0000000..22795f9 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/NetworkMonitor.kt @@ -0,0 +1,7 @@ +package top.fatweb.oxygen.toolbox.monitor + +import kotlinx.coroutines.flow.Flow + +interface NetworkMonitor { + val isOnline: Flow +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/TimeZoneBroadcastMonitor.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/TimeZoneBroadcastMonitor.kt new file mode 100644 index 0000000..0b0da77 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/TimeZoneBroadcastMonitor.kt @@ -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 = 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) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/TimeZoneMonitor.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/TimeZoneMonitor.kt new file mode 100644 index 0000000..028b726 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/monitor/TimeZoneMonitor.kt @@ -0,0 +1,8 @@ +package top.fatweb.oxygen.toolbox.monitor + +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.TimeZone + +interface TimeZoneMonitor { + val currentTimeZone: Flow +} diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt new file mode 100644 index 0000000..044ed27 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt @@ -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( + + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/SearchNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/SearchNavigation.kt new file mode 100644 index 0000000..a2c7714 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/SearchNavigation.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/StarNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/StarNavigation.kt new file mode 100644 index 0000000..ce8c316 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/StarNavigation.kt @@ -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 + ) { + + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt new file mode 100644 index 0000000..d3fac9b --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt @@ -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 + ) { } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt new file mode 100644 index 0000000..9a3a5b5 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/TopLevelDestination.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/OxygenDispatchers.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/OxygenDispatchers.kt new file mode 100644 index 0000000..50155aa --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/network/OxygenDispatchers.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/OfflineFirstUserDataRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/OfflineFirstUserDataRepository.kt new file mode 100644 index 0000000..3577623 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/OfflineFirstUserDataRepository.kt @@ -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 = + 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) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/UserDataRepository.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/UserDataRepository.kt new file mode 100644 index 0000000..1a418ca --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/repository/UserDataRepository.kt @@ -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 + + 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) +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt new file mode 100644 index 0000000..dfdf3f1 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt @@ -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, + 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, + 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 diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt new file mode 100644 index 0000000..757b465 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenAppState.kt @@ -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.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() +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/Background.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/Background.kt new file mode 100644 index 0000000..96d6c30 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/Background.kt @@ -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 = {}) + } +} diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/Navigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/Navigation.kt new file mode 100644 index 0000000..ccc6e1a --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/Navigation.kt @@ -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 = {} + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt new file mode 100644 index 0000000..1eef62e --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/OxygenTopAppBar.kt @@ -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" + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/search/SearchScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/search/SearchScreen.kt new file mode 100644 index 0000000..6a2dd24 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/search/SearchScreen.kt @@ -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") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/search/SearchViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/search/SearchViewModel.kt new file mode 100644 index 0000000..aba0fc2 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/search/SearchViewModel.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt new file mode 100644 index 0000000..ed884dc --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt @@ -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 = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..88219d0 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsViewModel.kt @@ -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 = + 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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/star/StarScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/star/StarScreen.kt new file mode 100644 index 0000000..7d220a8 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/star/StarScreen.kt @@ -0,0 +1,3 @@ +package top.fatweb.oxygen.toolbox.ui.star + +class StarScreen \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Background.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Background.kt new file mode 100644 index 0000000..83e5016 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Background.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Color.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Color.kt new file mode 100644 index 0000000..41f3c31 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Color.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Gradient.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Gradient.kt new file mode 100644 index 0000000..fa25259 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Gradient.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Theme.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Theme.kt new file mode 100644 index 0000000..59274fd --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Theme.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Tint.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Tint.kt new file mode 100644 index 0000000..75ca5ef --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Tint.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Type.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Type.kt new file mode 100644 index 0000000..5f1d0c2 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Type.kt @@ -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 + ) + ) +) \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tools/ToolsScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tools/ToolsScreen.kt new file mode 100644 index 0000000..7716e18 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tools/ToolsScreen.kt @@ -0,0 +1,8 @@ +package top.fatweb.oxygen.toolbox.ui.tools + +import androidx.compose.runtime.Composable + +@Composable +internal fun ToolsScreen() { + +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalTimeZone.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalTimeZone.kt new file mode 100644 index 0000000..66f6d30 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocalTimeZone.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocaleUtils.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocaleUtils.kt new file mode 100644 index 0000000..50b5982 --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/LocaleUtils.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt new file mode 100644 index 0000000..33b164b --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/proto/com/fatweb/oxygen/toolbox/data/dark_theme_config.proto b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/dark_theme_config.proto new file mode 100644 index 0000000..2314c85 --- /dev/null +++ b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/dark_theme_config.proto @@ -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; +} \ No newline at end of file diff --git a/app/src/main/proto/com/fatweb/oxygen/toolbox/data/language_config.proto b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/language_config.proto new file mode 100644 index 0000000..ab6f311 --- /dev/null +++ b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/language_config.proto @@ -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; +} \ No newline at end of file diff --git a/app/src/main/proto/com/fatweb/oxygen/toolbox/data/launch_page_config.proto b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/launch_page_config.proto new file mode 100644 index 0000000..de62ac0 --- /dev/null +++ b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/launch_page_config.proto @@ -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; +} \ No newline at end of file diff --git a/app/src/main/proto/com/fatweb/oxygen/toolbox/data/theme_brand_config.proto b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/theme_brand_config.proto new file mode 100644 index 0000000..68a9d04 --- /dev/null +++ b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/theme_brand_config.proto @@ -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; +} \ No newline at end of file diff --git a/app/src/main/proto/com/fatweb/oxygen/toolbox/data/user_preferences.proto b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/user_preferences.proto new file mode 100644 index 0000000..38f6a25 --- /dev/null +++ b/app/src/main/proto/com/fatweb/oxygen/toolbox/data/user_preferences.proto @@ -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; +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_oxygen.xml b/app/src/main/res/drawable/ic_oxygen.xml new file mode 100644 index 0000000..1a4e6e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_oxygen.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..cdc5f2c --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000..c8fc2e8 --- /dev/null +++ b/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,27 @@ + + + OxygenToolbox + ⚠️ 无法连接至互联网 + 工具 + 收藏 + 设置 + 加载中… + 语言 + 系统默认 + 启动页 + 工具 + 收藏 + 主题类型 + 默认 + Android + 深色模式 + 系统默认 + 明亮 + 深色 + 动态颜色 + 启用 + 禁用 + 更多 + 搜索 + 完成 + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..a0822b7 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,28 @@ + + OxygenToolbox + ⚠️ Unable to connect to the internet + Tools + Star + Settings + Loading… + Language + System Default + 中文 + English + Launch Page + Tools + Star + Theme Brand + Default + Android + Dark Mode + System Default + Light + Dark + Dynamic Color + Enable + Disable + More + Search + OK + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..515296c --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/kotlin/top/fatweb/oxygen/toolbox/ExampleUnitTest.kt b/app/src/test/kotlin/top/fatweb/oxygen/toolbox/ExampleUnitTest.kt new file mode 100644 index 0000000..69c7f56 --- /dev/null +++ b/app/src/test/kotlin/top/fatweb/oxygen/toolbox/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..38ae98b --- /dev/null +++ b/build.gradle.kts @@ -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 +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..55c4bc9 --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..77cd699 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0cc0fca --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..9cdc4eb --- /dev/null +++ b/settings.gradle.kts @@ -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") + \ No newline at end of file