Initialize the basic framework
@@ -0,0 +1,24 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("top.fatweb.oxygen.toolbox", appContext.packageName)
|
||||
}
|
||||
}
|
||||
33
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".OxygenApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Oxygen.Splash"
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="q" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
203
app/src/main/kotlin/top/fatweb/oxygen/toolbox/MainActivity.kt
Normal file
@@ -0,0 +1,203 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import top.fatweb.oxygen.toolbox.ui.OxygenApp
|
||||
import top.fatweb.oxygen.toolbox.ui.rememberOxygenAppState
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import top.fatweb.oxygen.toolbox.ui.util.LocalTimeZone
|
||||
import top.fatweb.oxygen.toolbox.ui.util.LocaleUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TAG = "MainActivity"
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||
|
||||
private val viewModel: MainActivityViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
var uiState: MainActivityUiState by mutableStateOf(MainActivityUiState.Loading)
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiState
|
||||
.onEach { uiState = it }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
when (uiState) {
|
||||
MainActivityUiState.Loading -> true
|
||||
is MainActivityUiState.Success -> false
|
||||
}
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
val locale = whatLocale(uiState)
|
||||
if (uiState != MainActivityUiState.Loading) {
|
||||
LaunchedEffect(locale) {
|
||||
LocaleUtils.switchLocale(this@MainActivity, locale)
|
||||
}
|
||||
}
|
||||
|
||||
val darkTheme = shouldUseDarkTheme(uiState)
|
||||
LaunchedEffect(darkTheme) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
android.graphics.Color.TRANSPARENT
|
||||
) { darkTheme },
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
lightScrim, darkScrim
|
||||
) { darkTheme }
|
||||
)
|
||||
}
|
||||
|
||||
val appState = rememberOxygenAppState(
|
||||
windowSizeClass = calculateWindowSizeClass(this),
|
||||
networkMonitor = networkMonitor,
|
||||
timeZoneMonitor = timeZoneMonitor,
|
||||
launchPageConfig = whatLaunchPage(uiState)
|
||||
)
|
||||
|
||||
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTimeZone provides currentTimeZone
|
||||
) {
|
||||
OxygenTheme(
|
||||
darkTheme = darkTheme,
|
||||
androidTheme = shouldUseAndroidTheme(uiState),
|
||||
dynamicColor = shouldUseDynamicColor(uiState)
|
||||
) {
|
||||
OxygenApp(appState)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "onCreate: C")
|
||||
}
|
||||
|
||||
Log.d(TAG, "onCreate: D")
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface UserDataRepositoryEntryPoint {
|
||||
val userDataRepository: UserDataRepository
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val userDataRepository =
|
||||
EntryPointAccessors.fromApplication<UserDataRepositoryEntryPoint>(newBase).userDataRepository
|
||||
super.attachBaseContext(LocaleUtils.attachBaseContext(newBase, runBlocking {
|
||||
userDataRepository.userData.first().languageConfig
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseDarkTheme(
|
||||
uiState: MainActivityUiState
|
||||
): Boolean = when (uiState) {
|
||||
MainActivityUiState.Loading -> isSystemInDarkTheme()
|
||||
is MainActivityUiState.Success -> when (uiState.userData.darkThemeConfig) {
|
||||
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
||||
DarkThemeConfig.LIGHT -> false
|
||||
DarkThemeConfig.DARK -> true
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseAndroidTheme(
|
||||
uiState: MainActivityUiState
|
||||
): Boolean = when (uiState) {
|
||||
MainActivityUiState.Loading -> false
|
||||
is MainActivityUiState.Success -> when (uiState.userData.themeBrandConfig) {
|
||||
ThemeBrandConfig.DEFAULT -> false
|
||||
ThemeBrandConfig.ANDROID -> true
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseDynamicColor(
|
||||
uiState: MainActivityUiState
|
||||
): Boolean = when (uiState) {
|
||||
MainActivityUiState.Loading -> true
|
||||
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun whatLocale(
|
||||
uiState: MainActivityUiState
|
||||
): LanguageConfig = when (uiState) {
|
||||
MainActivityUiState.Loading -> LanguageConfig.FOLLOW_SYSTEM
|
||||
is MainActivityUiState.Success -> uiState.userData.languageConfig
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun whatLaunchPage(
|
||||
uiState: MainActivityUiState
|
||||
): LaunchPageConfig = when (uiState) {
|
||||
MainActivityUiState.Loading -> LaunchPageConfig.TOOLS
|
||||
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* The default light scrim, as defined by androidx and the platform:
|
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
||||
*/
|
||||
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
||||
|
||||
/**
|
||||
* The default dark scrim, as defined by androidx and the platform:
|
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
||||
*/
|
||||
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
||||
@@ -0,0 +1,31 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import top.fatweb.oxygen.toolbox.model.UserData
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
userDataRepository: UserDataRepository
|
||||
) : ViewModel() {
|
||||
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
|
||||
MainActivityUiState.Success(it)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = MainActivityUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface MainActivityUiState {
|
||||
data object Loading : MainActivityUiState
|
||||
data class Success(val userData: UserData) : MainActivityUiState
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class OxygenApplication : Application() {
|
||||
@Inject
|
||||
lateinit var userDataRepository: UserDataRepository
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox.datastore
|
||||
|
||||
import androidx.datastore.core.DataMigration
|
||||
|
||||
internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
|
||||
override suspend fun cleanUp() = Unit
|
||||
|
||||
override suspend fun migrate(currentData: UserPreferences): UserPreferences =
|
||||
currentData.copy { }
|
||||
|
||||
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package top.fatweb.oxygen.toolbox.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.map
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.model.UserData
|
||||
import javax.inject.Inject
|
||||
|
||||
class OxygenPreferencesDataSource @Inject constructor(
|
||||
private val userPreferences: DataStore<UserPreferences>
|
||||
) {
|
||||
val userData = userPreferences.data
|
||||
.map {
|
||||
UserData(
|
||||
languageConfig = when (it.languageConfig) {
|
||||
null,
|
||||
LanguageConfigProto.UNRECOGNIZED,
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED,
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
||||
-> LanguageConfig.FOLLOW_SYSTEM
|
||||
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
||||
-> LanguageConfig.CHINESE
|
||||
|
||||
LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
||||
-> LanguageConfig.ENGLISH
|
||||
},
|
||||
launchPageConfig = when (it.launchPageConfig) {
|
||||
null,
|
||||
LaunchPageConfigProto.UNRECOGNIZED,
|
||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_UNSPECIFIED,
|
||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
||||
-> LaunchPageConfig.TOOLS
|
||||
|
||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
||||
-> LaunchPageConfig.STAR
|
||||
},
|
||||
themeBrandConfig = when (it.themeBrandConfig) {
|
||||
null,
|
||||
ThemeBrandConfigProto.UNRECOGNIZED,
|
||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_UNSPECIFIED,
|
||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
||||
->
|
||||
ThemeBrandConfig.DEFAULT
|
||||
|
||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
||||
-> ThemeBrandConfig.ANDROID
|
||||
},
|
||||
darkThemeConfig = when (it.darkThemeConfig) {
|
||||
null,
|
||||
DarkThemeConfigProto.UNRECOGNIZED,
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
||||
->
|
||||
DarkThemeConfig.FOLLOW_SYSTEM
|
||||
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
||||
-> DarkThemeConfig.LIGHT
|
||||
|
||||
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
||||
-> DarkThemeConfig.DARK
|
||||
},
|
||||
useDynamicColor = it.useDynamicColor
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setLanguageConfig(languageConfig: LanguageConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.languageConfig = when (languageConfig) {
|
||||
LanguageConfig.FOLLOW_SYSTEM -> LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
||||
LanguageConfig.CHINESE -> LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
||||
LanguageConfig.ENGLISH -> LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setLaunchPageConfig(launchPageConfig: LaunchPageConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.launchPageConfig = when (launchPageConfig) {
|
||||
LaunchPageConfig.TOOLS -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
||||
LaunchPageConfig.STAR -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setThemeBrandConfig(themeBrandConfig: ThemeBrandConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.themeBrandConfig = when (themeBrandConfig) {
|
||||
ThemeBrandConfig.DEFAULT -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
||||
ThemeBrandConfig.ANDROID -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.darkThemeConfig = when (darkThemeConfig) {
|
||||
DarkThemeConfig.FOLLOW_SYSTEM -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
||||
DarkThemeConfig.LIGHT -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
||||
DarkThemeConfig.DARK -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
|
||||
userPreferences.updateData {
|
||||
it.copy {
|
||||
this.useDynamicColor = useDynamicColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package top.fatweb.oxygen.toolbox.datastore
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserPreferencesSerializer @Inject constructor() : Serializer<UserPreferences> {
|
||||
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): UserPreferences =
|
||||
try {
|
||||
UserPreferences.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
|
||||
t.writeTo(output)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CoroutineScopesModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
@ApplicationScope
|
||||
fun providesCoroutineScope(
|
||||
@Dispatcher(OxygenDispatchers.Default) dispatcher: CoroutineDispatcher
|
||||
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import top.fatweb.oxygen.toolbox.monitor.ConnectivityManagerNetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||
import top.fatweb.oxygen.toolbox.repository.OfflineFirstUserDataRepository
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class DataModule {
|
||||
@Binds
|
||||
internal abstract fun bindsUserDataRepository(userDataRepository: OfflineFirstUserDataRepository): UserDataRepository
|
||||
|
||||
@Binds
|
||||
internal abstract fun bindsNetworkMonitor(networkMonitor: ConnectivityManagerNetworkMonitor): NetworkMonitor
|
||||
|
||||
@Binds
|
||||
internal abstract fun bindsTimeZoneMonitor(timeZoneMonitor: TimeZoneBroadcastMonitor): TimeZoneMonitor
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import top.fatweb.oxygen.toolbox.datastore.IntToStringIdsMigration
|
||||
import top.fatweb.oxygen.toolbox.datastore.UserPreferences
|
||||
import top.fatweb.oxygen.toolbox.datastore.UserPreferencesSerializer
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DataStoreModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun providesUserPreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@Dispatcher(OxygenDispatchers.IO) ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope scope: CoroutineScope,
|
||||
userPreferencesSerializer: UserPreferencesSerializer
|
||||
): DataStore<UserPreferences> =
|
||||
DataStoreFactory.create(
|
||||
serializer = userPreferencesSerializer,
|
||||
scope = CoroutineScope(scope.coroutineContext + ioDispatcher),
|
||||
migrations = listOf(
|
||||
IntToStringIdsMigration
|
||||
)
|
||||
) {
|
||||
context.dataStoreFile("user_preferences.pb")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package top.fatweb.oxygen.toolbox.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DispatchersModule {
|
||||
@Provides
|
||||
@Dispatcher(OxygenDispatchers.IO)
|
||||
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@Provides
|
||||
@Dispatcher(OxygenDispatchers.Default)
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package top.fatweb.oxygen.toolbox.icon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
|
||||
object OxygenIcons {
|
||||
val Home = Icons.Rounded.Home
|
||||
val HomeBorder = Icons.Outlined.Home
|
||||
val Star = Icons.Rounded.Star
|
||||
val StarBorder = Icons.Outlined.StarBorder
|
||||
val Search = Icons.Rounded.Search
|
||||
val MoreVert = Icons.Default.MoreVert
|
||||
val Back = Icons.Rounded.ArrowBackIosNew
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class DarkThemeConfig {
|
||||
FOLLOW_SYSTEM,
|
||||
LIGHT,
|
||||
DARK,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class LanguageConfig(val code: String? = null) {
|
||||
FOLLOW_SYSTEM,
|
||||
CHINESE("cn"),
|
||||
ENGLISH("en")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class LaunchPageConfig {
|
||||
TOOLS,
|
||||
STAR
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
enum class ThemeBrandConfig {
|
||||
DEFAULT,
|
||||
ANDROID
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package top.fatweb.oxygen.toolbox.model
|
||||
|
||||
data class UserData(
|
||||
val languageConfig: LanguageConfig,
|
||||
val launchPageConfig: LaunchPageConfig,
|
||||
val themeBrandConfig: ThemeBrandConfig,
|
||||
val darkThemeConfig: DarkThemeConfig,
|
||||
val useDynamicColor: Boolean
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.core.content.getSystemService
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ConnectivityManagerNetworkMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : NetworkMonitor {
|
||||
override val isOnline: Flow<Boolean> = callbackFlow {
|
||||
val connectivityManager = context.getSystemService<ConnectivityManager>()
|
||||
if (connectivityManager == null) {
|
||||
channel.trySend(false)
|
||||
channel.close()
|
||||
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
val callback = object : NetworkCallback() {
|
||||
private val networks = mutableSetOf<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
networks += network
|
||||
channel.trySend(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
networks -= network
|
||||
channel.trySend(networks.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
val request =
|
||||
NetworkRequest
|
||||
.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
|
||||
channel.trySend(connectivityManager.isCurrentlyConnected())
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
}
|
||||
.conflate()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun ConnectivityManager.isCurrentlyConnected() = when {
|
||||
VERSION.SDK_INT >= VERSION_CODES.M ->
|
||||
activeNetwork
|
||||
?.let(::getNetworkCapabilities)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
|
||||
else -> activeNetworkInfo?.isConnected
|
||||
} ?: false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
val isOnline: Flow<Boolean>
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toKotlinTimeZone
|
||||
import top.fatweb.oxygen.toolbox.di.ApplicationScope
|
||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||
import java.time.ZoneId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Singleton
|
||||
class TimeZoneBroadcastMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@ApplicationScope appScope: CoroutineScope,
|
||||
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
|
||||
) : TimeZoneMonitor {
|
||||
override val currentTimeZone: SharedFlow<TimeZone> = callbackFlow {
|
||||
trySend(TimeZone.currentSystemDefault())
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_TIMEZONE_CHANGED) return
|
||||
|
||||
val zonIdFromIntent = if (Build.VERSION.SDK_INT < VERSION_CODES.R) {
|
||||
null
|
||||
} else {
|
||||
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.let { timeZoneId ->
|
||||
val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)
|
||||
zoneId.toKotlinTimeZone()
|
||||
}
|
||||
}
|
||||
|
||||
trySend(zonIdFromIntent ?: TimeZone.currentSystemDefault())
|
||||
}
|
||||
}
|
||||
|
||||
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
|
||||
|
||||
trySend(TimeZone.currentSystemDefault())
|
||||
|
||||
awaitClose {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.conflate()
|
||||
.flowOn(ioDispatcher)
|
||||
.shareIn(appScope, SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), 1)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package top.fatweb.oxygen.toolbox.monitor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
||||
interface TimeZoneMonitor {
|
||||
val currentTimeZone: Flow<TimeZone>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import top.fatweb.oxygen.toolbox.ui.OxygenAppState
|
||||
|
||||
@Composable
|
||||
fun OxygenNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
appState: OxygenAppState,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||
startDestination: String
|
||||
) {
|
||||
val navController = appState.navController
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
searchScreen(
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
toolsScreen(
|
||||
|
||||
)
|
||||
starScreen(
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import top.fatweb.oxygen.toolbox.ui.search.SearchRoute
|
||||
|
||||
const val SEARCH_ROUTE = "search_route"
|
||||
|
||||
fun NavController.navigateToSearch(navOptions: NavOptions? = null) =
|
||||
navigate(SEARCH_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.searchScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
composable(
|
||||
route = SEARCH_ROUTE
|
||||
) {
|
||||
SearchRoute(onBackClick = onBackClick)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
const val STAR_ROUTE = "star_route"
|
||||
|
||||
fun NavController.navigateToStar(navOptions: NavOptions) = navigate(STAR_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.starScreen() {
|
||||
composable(
|
||||
route = STAR_ROUTE
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
const val TOOLS_ROUTE = "tools_route"
|
||||
|
||||
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.toolsScreen() {
|
||||
composable(
|
||||
route = TOOLS_ROUTE
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package top.fatweb.oxygen.toolbox.navigation
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
|
||||
enum class TopLevelDestination(
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector,
|
||||
val iconTextId: Int,
|
||||
val titleTextId: Int
|
||||
) {
|
||||
TOOLS(
|
||||
selectedIcon = OxygenIcons.Home,
|
||||
unselectedIcon = OxygenIcons.HomeBorder,
|
||||
iconTextId = R.string.feature_tools_title,
|
||||
titleTextId = R.string.feature_tools_title
|
||||
),
|
||||
|
||||
STAR(
|
||||
selectedIcon = OxygenIcons.Star,
|
||||
unselectedIcon = OxygenIcons.StarBorder,
|
||||
iconTextId = R.string.feature_star_title,
|
||||
titleTextId = R.string.feature_star_title
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox.network
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Dispatcher(val oxygenDispatcher: OxygenDispatchers)
|
||||
|
||||
enum class OxygenDispatchers {
|
||||
Default,
|
||||
IO
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package top.fatweb.oxygen.toolbox.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import top.fatweb.oxygen.toolbox.datastore.OxygenPreferencesDataSource
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.model.UserData
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class OfflineFirstUserDataRepository @Inject constructor(
|
||||
private val oxygenPreferencesDataSource: OxygenPreferencesDataSource
|
||||
) : UserDataRepository {
|
||||
override val userData: Flow<UserData> =
|
||||
oxygenPreferencesDataSource.userData
|
||||
|
||||
override suspend fun setLanguageConfig(languageConfig: LanguageConfig) {
|
||||
oxygenPreferencesDataSource.setLanguageConfig(languageConfig)
|
||||
}
|
||||
|
||||
override suspend fun setLaunchPageConfig(launchPageConfig: LaunchPageConfig) {
|
||||
oxygenPreferencesDataSource.setLaunchPageConfig(launchPageConfig)
|
||||
}
|
||||
|
||||
override suspend fun setThemeBrandConfig(themeBrandConfig: ThemeBrandConfig) {
|
||||
oxygenPreferencesDataSource.setThemeBrandConfig(themeBrandConfig)
|
||||
}
|
||||
|
||||
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
oxygenPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
|
||||
}
|
||||
|
||||
override suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
|
||||
oxygenPreferencesDataSource.setUseDynamicColor(useDynamicColor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package top.fatweb.oxygen.toolbox.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.model.UserData
|
||||
|
||||
interface UserDataRepository {
|
||||
val userData: Flow<UserData>
|
||||
|
||||
suspend fun setLanguageConfig(languageConfig: LanguageConfig)
|
||||
|
||||
suspend fun setLaunchPageConfig(launchPageConfig: LaunchPageConfig)
|
||||
|
||||
suspend fun setThemeBrandConfig(themeBrandConfig: ThemeBrandConfig)
|
||||
|
||||
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
|
||||
|
||||
suspend fun setUseDynamicColor(useDynamicColor: Boolean)
|
||||
}
|
||||
236
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt
Normal file
@@ -0,0 +1,236 @@
|
||||
package top.fatweb.oxygen.toolbox.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.navigation.OxygenNavHost
|
||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenBackground
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenGradientBackground
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBar
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBarItem
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem
|
||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
||||
import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OxygenApp(appState: OxygenAppState) {
|
||||
val shouldShowGradientBackground =
|
||||
appState.currentTopLevelDestination == TopLevelDestination.TOOLS
|
||||
var showSettingsDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
OxygenBackground {
|
||||
OxygenGradientBackground(
|
||||
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
|
||||
) {
|
||||
val destination = appState.currentTopLevelDestination
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
||||
|
||||
val noConnectMessage = stringResource(R.string.no_connect)
|
||||
|
||||
LaunchedEffect(isOffline) {
|
||||
if (isOffline) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = noConnectMessage,
|
||||
duration = SnackbarDuration.Indefinite
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSettingsDialog) {
|
||||
SettingsDialog(
|
||||
onDismiss = { showSettingsDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
bottomBar = {
|
||||
if (appState.shouldShowBottomBar && destination != null) {
|
||||
OxygenBottomBar(
|
||||
destinations = appState.topLevelDestinations,
|
||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
||||
currentDestination = appState.currentDestination
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Horizontal
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (appState.shouldShowNavRail && destination != null) {
|
||||
OxygenNavRail(
|
||||
modifier = Modifier.safeDrawingPadding(),
|
||||
destinations = appState.topLevelDestinations,
|
||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
||||
currentDestination = appState.currentDestination
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize()
|
||||
) {
|
||||
if (destination != null) {
|
||||
OxygenTopAppBar(
|
||||
titleRes = destination.titleTextId,
|
||||
navigationIcon = OxygenIcons.Search,
|
||||
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
|
||||
actionIcon = OxygenIcons.MoreVert,
|
||||
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
onNavigationClick = { appState.navigateToSearch() },
|
||||
onActionClick = { showSettingsDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
OxygenNavHost(
|
||||
appState = appState,
|
||||
onShowSnackbar = { message, action ->
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = action,
|
||||
duration = SnackbarDuration.Short
|
||||
) == SnackbarResult.ActionPerformed
|
||||
},
|
||||
startDestination = when (appState.launchPageConfig) {
|
||||
LaunchPageConfig.TOOLS -> TOOLS_ROUTE
|
||||
LaunchPageConfig.STAR -> STAR_ROUTE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OxygenBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
destinations: List<TopLevelDestination>,
|
||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
||||
currentDestination: NavDestination?
|
||||
) {
|
||||
OxygenNavigationBar(
|
||||
modifier = modifier
|
||||
) {
|
||||
destinations.forEach { destination ->
|
||||
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
||||
OxygenNavigationBarItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = { Text(stringResource(destination.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = destination.unselectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = destination.selectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onNavigateToDestination(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OxygenNavRail(
|
||||
modifier: Modifier = Modifier,
|
||||
destinations: List<TopLevelDestination>,
|
||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
||||
currentDestination: NavDestination?
|
||||
) {
|
||||
OxygenNavigationRail(
|
||||
modifier = modifier
|
||||
) {
|
||||
destinations.forEach { destination ->
|
||||
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
||||
OxygenNavigationRailItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = { Text(stringResource(destination.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = destination.unselectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = destination.selectedIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onNavigateToDestination(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
|
||||
this?.hierarchy?.any {
|
||||
it.route?.contains(destination.name, true) ?: false
|
||||
} ?: false
|
||||
@@ -0,0 +1,117 @@
|
||||
package top.fatweb.oxygen.toolbox.ui
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.datetime.TimeZone
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun rememberOxygenAppState(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
timeZoneMonitor: TimeZoneMonitor,
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
launchPageConfig: LaunchPageConfig
|
||||
): OxygenAppState = remember(
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
timeZoneMonitor,
|
||||
coroutineScope,
|
||||
navController,
|
||||
launchPageConfig
|
||||
) {
|
||||
OxygenAppState(
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
timeZoneMonitor,
|
||||
coroutineScope,
|
||||
navController,
|
||||
launchPageConfig
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
class OxygenAppState(
|
||||
val windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
timeZoneMonitor: TimeZoneMonitor,
|
||||
coroutineScope: CoroutineScope,
|
||||
val navController: NavHostController,
|
||||
val launchPageConfig: LaunchPageConfig
|
||||
) {
|
||||
val currentDestination: NavDestination?
|
||||
@Composable get() = navController
|
||||
.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
val currentTopLevelDestination: TopLevelDestination?
|
||||
@Composable get() = when (currentDestination?.route) {
|
||||
TOOLS_ROUTE -> TopLevelDestination.TOOLS
|
||||
STAR_ROUTE -> TopLevelDestination.STAR
|
||||
else -> null
|
||||
}
|
||||
|
||||
val shouldShowBottomBar: Boolean
|
||||
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact
|
||||
|
||||
val shouldShowNavRail: Boolean
|
||||
get() = !shouldShowBottomBar
|
||||
|
||||
val isOffline = networkMonitor.isOnline
|
||||
.map(Boolean::not)
|
||||
.stateIn(
|
||||
scope = coroutineScope,
|
||||
initialValue = false,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.entries
|
||||
|
||||
val currentTimeZone = timeZoneMonitor.currentTimeZone
|
||||
.stateIn(
|
||||
scope = coroutineScope,
|
||||
initialValue = TimeZone.currentSystemDefault(),
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
|
||||
val topLevelNavOptions = navOptions {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
when (topLevelDestination) {
|
||||
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
|
||||
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToSearch() = navController.navigateToSearch()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalBackgroundTheme
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import kotlin.math.tan
|
||||
|
||||
@Composable
|
||||
fun OxygenBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val color = LocalBackgroundTheme.current.color
|
||||
val tonalElevation = LocalBackgroundTheme.current.tonalElevation
|
||||
Surface(
|
||||
color = if (color == Color.Unspecified) Color.Transparent else color,
|
||||
tonalElevation = if (tonalElevation == Dp.Unspecified) 0.dp else tonalElevation,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
CompositionLocalProvider(LocalAbsoluteTonalElevation provides 0.dp) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenGradientBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
gradientColors: GradientColors = LocalGradientColors.current,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val currentTopColor by rememberUpdatedState(gradientColors.top)
|
||||
val currentBottomColor by rememberUpdatedState(gradientColors.bottom)
|
||||
Surface(
|
||||
color = if (gradientColors.container == Color.Unspecified) Color.Transparent else gradientColors.container,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.drawWithCache {
|
||||
val offset = size.height * tan(
|
||||
Math
|
||||
.toRadians(11.06)
|
||||
.toFloat()
|
||||
)
|
||||
|
||||
val start = Offset(size.width / 2 + offset / 2, 0f)
|
||||
val end = Offset(size.width / 2 - offset / 2, size.height)
|
||||
|
||||
val topGradient = Brush.linearGradient(
|
||||
0f to if (currentTopColor == Color.Unspecified) Color.Transparent else currentTopColor,
|
||||
0.724f to Color.Transparent,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
|
||||
val bottomGradient = Brush.linearGradient(
|
||||
0.2552f to Color.Transparent,
|
||||
1f to if (currentBottomColor == Color.Unspecified) Color.Transparent else currentBottomColor,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
|
||||
onDrawBehind {
|
||||
drawRect(topGradient)
|
||||
drawRect(bottomGradient)
|
||||
}
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
|
||||
annotation class ThemePreviews
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BackgroundDefault() {
|
||||
OxygenTheme(dynamicColor = false) {
|
||||
OxygenBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BackgroundDynamic() {
|
||||
OxygenTheme(dynamicColor = true) {
|
||||
OxygenBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BackgroundAndroid() {
|
||||
OxygenTheme(androidTheme = true) {
|
||||
OxygenBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun GradientBackgroundDefault() {
|
||||
OxygenTheme(dynamicColor = false) {
|
||||
OxygenGradientBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun GradientBackgroundDynamic() {
|
||||
OxygenTheme(dynamicColor = true) {
|
||||
OxygenGradientBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun GradientBackgroundAndroid() {
|
||||
OxygenTheme(androidTheme = true) {
|
||||
OxygenGradientBackground(Modifier.size(100.dp), content = {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.NavigationRailItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
|
||||
@Composable
|
||||
fun RowScope.OxygenNavigationBarItem(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable () -> Unit,
|
||||
selectedIcon: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
alwaysShowLabel: Boolean = false
|
||||
) {
|
||||
NavigationBarItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = label,
|
||||
icon = if (selected) selectedIcon else icon,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
alwaysShowLabel = alwaysShowLabel,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedIconColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenNavigationBar(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
NavigationBar(
|
||||
modifier = modifier,
|
||||
contentColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
content = content,
|
||||
tonalElevation = 0.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenNavigationRailItem(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable () -> Unit,
|
||||
selectedIcon: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
alwaysShowLabel: Boolean = true
|
||||
) {
|
||||
NavigationRailItem(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
label = label,
|
||||
icon = if (selected) selectedIcon else icon,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
alwaysShowLabel = alwaysShowLabel,
|
||||
colors = NavigationRailItemDefaults.colors(
|
||||
selectedIconColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedIconColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OxygenNavigationRail(
|
||||
modifier: Modifier = Modifier,
|
||||
header: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
NavigationRail(
|
||||
modifier = modifier,
|
||||
header = header,
|
||||
contentColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||
content = content,
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
}
|
||||
|
||||
object OxygenNavigationDefaults {
|
||||
@Composable
|
||||
fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
|
||||
@Composable
|
||||
fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
@Composable
|
||||
fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun OxygenNavigationBarPreview() {
|
||||
val items = TopLevelDestination.entries
|
||||
|
||||
OxygenTheme {
|
||||
OxygenNavigationBar {
|
||||
items.forEachIndexed { index, item ->
|
||||
OxygenNavigationBarItem(
|
||||
selected = index == 0,
|
||||
label = { Text(stringResource(item.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = item.unselectedIcon,
|
||||
contentDescription = stringResource(item.titleTextId)
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = item.selectedIcon, contentDescription = stringResource(
|
||||
item.titleTextId
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun OxygenNavigationRailPreview() {
|
||||
val items = TopLevelDestination.entries
|
||||
|
||||
OxygenTheme {
|
||||
OxygenNavigationRail {
|
||||
items.forEachIndexed { index, item ->
|
||||
OxygenNavigationRailItem(
|
||||
selected = index == 0,
|
||||
label = { Text(stringResource(item.titleTextId)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = item.unselectedIcon,
|
||||
contentDescription = stringResource(item.titleTextId)
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = item.selectedIcon, contentDescription = stringResource(
|
||||
item.titleTextId
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.component
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import android.R as androidR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OxygenTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes titleRes: Int,
|
||||
navigationIcon: ImageVector,
|
||||
navigationIconContentDescription: String,
|
||||
actionIcon: ImageVector,
|
||||
actionIconContentDescription: String,
|
||||
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onActionClick: () -> Unit = {}
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier,
|
||||
title = { Text(stringResource(titleRes)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigationClick) {
|
||||
Icon(
|
||||
imageVector = navigationIcon,
|
||||
contentDescription = navigationIconContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onActionClick) {
|
||||
Icon(
|
||||
imageVector = actionIcon,
|
||||
contentDescription = actionIconContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OxygenTopAppBarPreview() {
|
||||
OxygenTheme {
|
||||
OxygenTopAppBar(
|
||||
titleRes = androidR.string.untitled,
|
||||
navigationIcon = OxygenIcons.Search,
|
||||
navigationIconContentDescription = "Navigation icon",
|
||||
actionIcon = OxygenIcons.MoreVert,
|
||||
actionIconContentDescription = "Action icon"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.search
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
|
||||
@Composable
|
||||
internal fun SearchRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClick: () -> Unit,
|
||||
// searchViewmodel: SearchViewModel = hiltViewModel()
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding()
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(imageVector = OxygenIcons.Back, contentDescription = null)
|
||||
}
|
||||
Text("Search")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor(
|
||||
) : ViewModel()
|
||||
@@ -0,0 +1,301 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.ui.component.ThemePreviews
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.supportsDynamicTheming
|
||||
|
||||
@Composable
|
||||
fun SettingsDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: SettingsViewModel = hiltViewModel()
|
||||
) {
|
||||
val settingsUiState by viewModel.settingsUiState.collectAsStateWithLifecycle()
|
||||
SettingsDialog(
|
||||
modifier = modifier,
|
||||
settingsUiState = settingsUiState,
|
||||
onDismiss = onDismiss,
|
||||
onChangeLanguageConfig = viewModel::updateLanguageConfig,
|
||||
onChangeLaunchPageConfig = viewModel::updateLaunchPageConfig,
|
||||
onchangeThemeBrandConfig = viewModel::updateThemeBrandConfig,
|
||||
onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig,
|
||||
onchangeUseDynamicColor = viewModel::updateUseDynamicColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
settingsUiState: SettingsUiState,
|
||||
onDismiss: () -> Unit,
|
||||
supportDynamicColor: Boolean = supportsDynamicTheming(),
|
||||
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
||||
onChangeLaunchPageConfig: (launchPageConfig: LaunchPageConfig) -> Unit,
|
||||
onchangeThemeBrandConfig: (themeBrandConfig: ThemeBrandConfig) -> Unit,
|
||||
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
|
||||
onchangeUseDynamicColor: (useDynamicColor: Boolean) -> Unit
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
AlertDialog(
|
||||
modifier = modifier
|
||||
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
|
||||
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.feature_settings_title),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
text = {
|
||||
HorizontalDivider()
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (settingsUiState) {
|
||||
SettingsUiState.Loading -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
text = stringResource(R.string.feature_settings_loading)
|
||||
)
|
||||
}
|
||||
|
||||
is SettingsUiState.Success -> {
|
||||
SettingsPanel(
|
||||
settings = settingsUiState.settings,
|
||||
supportDynamicColor = supportDynamicColor,
|
||||
onChangeLanguageConfig = onChangeLanguageConfig,
|
||||
onChangeLaunchPageConfig = onChangeLaunchPageConfig,
|
||||
onchangeThemeBrandConfig = onchangeThemeBrandConfig,
|
||||
onChangeDarkThemeConfig = onChangeDarkThemeConfig,
|
||||
onchangeUseDynamicColor = onchangeUseDynamicColor
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable { onDismiss() },
|
||||
text = stringResource(R.string.feature_settings_dismiss_dialog_button_text),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SettingsPanel(
|
||||
settings: UserEditableSettings,
|
||||
supportDynamicColor: Boolean,
|
||||
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
||||
onChangeLaunchPageConfig: (launchPageConfig: LaunchPageConfig) -> Unit,
|
||||
onchangeThemeBrandConfig: (themeBrandConfig: ThemeBrandConfig) -> Unit,
|
||||
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
|
||||
onchangeUseDynamicColor: (useDynamicColor: Boolean) -> Unit
|
||||
) {
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_language))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_language_system_default),
|
||||
selected = settings.languageConfig == LanguageConfig.FOLLOW_SYSTEM,
|
||||
onClick = { onChangeLanguageConfig(LanguageConfig.FOLLOW_SYSTEM) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_language_chinese),
|
||||
selected = settings.languageConfig == LanguageConfig.CHINESE,
|
||||
onClick = { onChangeLanguageConfig(LanguageConfig.CHINESE) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_language_english),
|
||||
selected = settings.languageConfig == LanguageConfig.ENGLISH,
|
||||
onClick = { onChangeLanguageConfig(LanguageConfig.ENGLISH) }
|
||||
)
|
||||
}
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_launch_page))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_launch_page_tools),
|
||||
selected = settings.launchPageConfig == LaunchPageConfig.TOOLS,
|
||||
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.TOOLS) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_launch_page_star),
|
||||
selected = settings.launchPageConfig == LaunchPageConfig.STAR,
|
||||
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.STAR) }
|
||||
)
|
||||
}
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_theme_brand))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_theme_brand_default),
|
||||
selected = settings.themeBrandConfig == ThemeBrandConfig.DEFAULT,
|
||||
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.DEFAULT) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_theme_brand_android),
|
||||
selected = settings.themeBrandConfig == ThemeBrandConfig.ANDROID,
|
||||
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.ANDROID) }
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = settings.themeBrandConfig == ThemeBrandConfig.DEFAULT && supportDynamicColor) {
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_dynamic_color))
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dynamic_color_enable),
|
||||
selected = settings.useDynamicColor,
|
||||
onClick = { onchangeUseDynamicColor(true) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dynamic_color_disable),
|
||||
selected = !settings.useDynamicColor,
|
||||
onClick = { onchangeUseDynamicColor(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_dark_mode))
|
||||
Column(
|
||||
modifier = Modifier.selectableGroup()
|
||||
) {
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dark_mode_system_default),
|
||||
selected = settings.darkThemeConfig == DarkThemeConfig.FOLLOW_SYSTEM,
|
||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.FOLLOW_SYSTEM) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dark_mode_light),
|
||||
selected = settings.darkThemeConfig == DarkThemeConfig.LIGHT,
|
||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.LIGHT) }
|
||||
)
|
||||
SettingsDialogThemeChooserRow(
|
||||
text = stringResource(R.string.feature_settings_dark_mode_dark),
|
||||
selected = settings.darkThemeConfig == DarkThemeConfig.DARK,
|
||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.DARK) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDialogSectionTitle(text: String) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDialogThemeChooserRow(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.selectable(
|
||||
selected = selected,
|
||||
role = Role.RadioButton,
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun SettingsDialogLoadingPreview() {
|
||||
OxygenTheme {
|
||||
SettingsDialog(
|
||||
onDismiss = { },
|
||||
settingsUiState = SettingsUiState.Loading,
|
||||
onChangeLanguageConfig = {},
|
||||
onChangeLaunchPageConfig = {},
|
||||
onchangeThemeBrandConfig = {},
|
||||
onChangeDarkThemeConfig = {},
|
||||
onchangeUseDynamicColor = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun SettingDialogPreview() {
|
||||
OxygenTheme {
|
||||
SettingsDialog(
|
||||
onDismiss = {},
|
||||
settingsUiState = SettingsUiState.Success(
|
||||
UserEditableSettings(
|
||||
languageConfig = LanguageConfig.FOLLOW_SYSTEM,
|
||||
launchPageConfig = LaunchPageConfig.TOOLS,
|
||||
themeBrandConfig = ThemeBrandConfig.DEFAULT,
|
||||
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
|
||||
useDynamicColor = true
|
||||
)
|
||||
),
|
||||
onChangeLanguageConfig = {},
|
||||
onChangeLaunchPageConfig = {},
|
||||
onchangeThemeBrandConfig = {},
|
||||
onChangeDarkThemeConfig = {},
|
||||
onchangeUseDynamicColor = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val userDataRepository: UserDataRepository
|
||||
) : ViewModel() {
|
||||
val settingsUiState: StateFlow<SettingsUiState> =
|
||||
userDataRepository.userData
|
||||
.map { userData ->
|
||||
SettingsUiState.Success(
|
||||
settings = UserEditableSettings(
|
||||
languageConfig = userData.languageConfig,
|
||||
launchPageConfig = userData.launchPageConfig,
|
||||
themeBrandConfig = userData.themeBrandConfig,
|
||||
darkThemeConfig = userData.darkThemeConfig,
|
||||
useDynamicColor = userData.useDynamicColor
|
||||
)
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = SettingsUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
fun updateLanguageConfig(languageConfig: LanguageConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setLanguageConfig(languageConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLaunchPageConfig(launchPageConfig: LaunchPageConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setLaunchPageConfig(launchPageConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateThemeBrandConfig(themeBrandConfig: ThemeBrandConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setThemeBrandConfig(themeBrandConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setDarkThemeConfig(darkThemeConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUseDynamicColor(useDynamicColor: Boolean) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setUseDynamicColor(useDynamicColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UserEditableSettings(
|
||||
val languageConfig: LanguageConfig,
|
||||
val launchPageConfig: LaunchPageConfig,
|
||||
val themeBrandConfig: ThemeBrandConfig,
|
||||
val darkThemeConfig: DarkThemeConfig,
|
||||
val useDynamicColor: Boolean
|
||||
)
|
||||
|
||||
sealed interface SettingsUiState {
|
||||
data object Loading : SettingsUiState
|
||||
data class Success(val settings: UserEditableSettings) : SettingsUiState
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.star
|
||||
|
||||
class StarScreen
|
||||
@@ -0,0 +1,14 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Immutable
|
||||
data class BackgroundTheme(
|
||||
val color: Color = Color.Unspecified,
|
||||
val tonalElevation: Dp = Dp.Unspecified
|
||||
)
|
||||
|
||||
val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() }
|
||||
@@ -0,0 +1,66 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal val Blue10 = Color(0xFF001F28)
|
||||
internal val Blue20 = Color(0xFF003544)
|
||||
internal val Blue30 = Color(0xFF004D61)
|
||||
internal val Blue40 = Color(0xFF006780)
|
||||
internal val Blue80 = Color(0xFF5DD5FC)
|
||||
internal val Blue90 = Color(0xFFB8EAFF)
|
||||
internal val DarkGreen10 = Color(0xFF0D1F12)
|
||||
internal val DarkGreen20 = Color(0xFF223526)
|
||||
internal val DarkGreen30 = Color(0xFF394B3C)
|
||||
internal val DarkGreen40 = Color(0xFF4F6352)
|
||||
internal val DarkGreen80 = Color(0xFFB7CCB8)
|
||||
internal val DarkGreen90 = Color(0xFFD3E8D3)
|
||||
internal val DarkGreenGray10 = Color(0xFF1A1C1A)
|
||||
internal val DarkGreenGray20 = Color(0xFF2F312E)
|
||||
internal val DarkGreenGray90 = Color(0xFFE2E3DE)
|
||||
internal val DarkGreenGray95 = Color(0xFFF0F1EC)
|
||||
internal val DarkGreenGray99 = Color(0xFFFBFDF7)
|
||||
internal val DarkPurpleGray10 = Color(0xFF201A1B)
|
||||
internal val DarkPurpleGray20 = Color(0xFF362F30)
|
||||
internal val DarkPurpleGray90 = Color(0xFFECDFE0)
|
||||
internal val DarkPurpleGray95 = Color(0xFFFAEEEF)
|
||||
internal val DarkPurpleGray99 = Color(0xFFFCFCFC)
|
||||
internal val Green10 = Color(0xFF00210B)
|
||||
internal val Green20 = Color(0xFF003919)
|
||||
internal val Green30 = Color(0xFF005227)
|
||||
internal val Green40 = Color(0xFF006D36)
|
||||
internal val Green80 = Color(0xFF0EE37C)
|
||||
internal val Green90 = Color(0xFF5AFF9D)
|
||||
internal val GreenGray30 = Color(0xFF414941)
|
||||
internal val GreenGray50 = Color(0xFF727971)
|
||||
internal val GreenGray60 = Color(0xFF8B938A)
|
||||
internal val GreenGray80 = Color(0xFFC1C9BF)
|
||||
internal val GreenGray90 = Color(0xFFDDE5DB)
|
||||
internal val Orange10 = Color(0xFF380D00)
|
||||
internal val Orange20 = Color(0xFF5B1A00)
|
||||
internal val Orange30 = Color(0xFF812800)
|
||||
internal val Orange40 = Color(0xFFA23F16)
|
||||
internal val Orange80 = Color(0xFFFFB59B)
|
||||
internal val Orange90 = Color(0xFFFFDBCF)
|
||||
internal val Purple10 = Color(0xFF36003C)
|
||||
internal val Purple20 = Color(0xFF560A5D)
|
||||
internal val Purple30 = Color(0xFF702776)
|
||||
internal val Purple40 = Color(0xFF8B418F)
|
||||
internal val Purple80 = Color(0xFFFFA9FE)
|
||||
internal val Purple90 = Color(0xFFFFD6FA)
|
||||
internal val PurpleGray30 = Color(0xFF4D444C)
|
||||
internal val PurpleGray50 = Color(0xFF7F747C)
|
||||
internal val PurpleGray60 = Color(0xFF998D96)
|
||||
internal val PurpleGray80 = Color(0xFFD0C3CC)
|
||||
internal val PurpleGray90 = Color(0xFFEDDEE8)
|
||||
internal val Red10 = Color(0xFF410002)
|
||||
internal val Red20 = Color(0xFF690005)
|
||||
internal val Red30 = Color(0xFF93000A)
|
||||
internal val Red40 = Color(0xFFBA1A1A)
|
||||
internal val Red80 = Color(0xFFFFB4AB)
|
||||
internal val Red90 = Color(0xFFFFDAD6)
|
||||
internal val Teal10 = Color(0xFF001F26)
|
||||
internal val Teal20 = Color(0xFF02363F)
|
||||
internal val Teal30 = Color(0xFF214D56)
|
||||
internal val Teal40 = Color(0xFF3A656F)
|
||||
internal val Teal80 = Color(0xFFA2CED9)
|
||||
internal val Teal90 = Color(0xFFBEEAF6)
|
||||
@@ -0,0 +1,14 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
data class GradientColors(
|
||||
val top: Color = Color.Unspecified,
|
||||
val bottom: Color = Color.Unspecified,
|
||||
val container: Color = Color.Unspecified
|
||||
)
|
||||
|
||||
val LocalGradientColors = staticCompositionLocalOf { GradientColors() }
|
||||
194
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val LightDefaultColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Purple90,
|
||||
onPrimaryContainer = Purple10,
|
||||
secondary = Orange40,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = Orange90,
|
||||
onSecondaryContainer = Orange10,
|
||||
tertiary = Blue40,
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = Blue90,
|
||||
onTertiaryContainer = Blue10,
|
||||
error = Red40,
|
||||
onError = Color.White,
|
||||
errorContainer = Red90,
|
||||
onErrorContainer = Red10,
|
||||
background = DarkPurpleGray99,
|
||||
onBackground = DarkPurpleGray10,
|
||||
surface = DarkPurpleGray99,
|
||||
onSurface = DarkPurpleGray10,
|
||||
surfaceVariant = PurpleGray90,
|
||||
onSurfaceVariant = PurpleGray30,
|
||||
inverseSurface = DarkPurpleGray20,
|
||||
inverseOnSurface = DarkPurpleGray95,
|
||||
outline = PurpleGray50,
|
||||
)
|
||||
|
||||
val DarkDefaultColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
onPrimary = Purple20,
|
||||
primaryContainer = Purple30,
|
||||
onPrimaryContainer = Purple90,
|
||||
secondary = Orange80,
|
||||
onSecondary = Orange20,
|
||||
secondaryContainer = Orange30,
|
||||
onSecondaryContainer = Orange90,
|
||||
tertiary = Blue80,
|
||||
onTertiary = Blue20,
|
||||
tertiaryContainer = Blue30,
|
||||
onTertiaryContainer = Blue90,
|
||||
error = Red80,
|
||||
onError = Red20,
|
||||
errorContainer = Red30,
|
||||
onErrorContainer = Red90,
|
||||
background = DarkPurpleGray10,
|
||||
onBackground = DarkPurpleGray90,
|
||||
surface = DarkPurpleGray10,
|
||||
onSurface = DarkPurpleGray90,
|
||||
surfaceVariant = PurpleGray30,
|
||||
onSurfaceVariant = PurpleGray80,
|
||||
inverseSurface = DarkPurpleGray90,
|
||||
inverseOnSurface = DarkPurpleGray10,
|
||||
outline = PurpleGray60,
|
||||
)
|
||||
|
||||
val LightAndroidColorScheme = lightColorScheme(
|
||||
primary = Green40,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Green90,
|
||||
onPrimaryContainer = Green10,
|
||||
secondary = DarkGreen40,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = DarkGreen90,
|
||||
onSecondaryContainer = DarkGreen10,
|
||||
tertiary = Teal40,
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = Teal90,
|
||||
onTertiaryContainer = Teal10,
|
||||
error = Red40,
|
||||
onError = Color.White,
|
||||
errorContainer = Red90,
|
||||
onErrorContainer = Red10,
|
||||
background = DarkGreenGray99,
|
||||
onBackground = DarkGreenGray10,
|
||||
surface = DarkGreenGray99,
|
||||
onSurface = DarkGreenGray10,
|
||||
surfaceVariant = GreenGray90,
|
||||
onSurfaceVariant = GreenGray30,
|
||||
inverseSurface = DarkGreenGray20,
|
||||
inverseOnSurface = DarkGreenGray95,
|
||||
outline = GreenGray50,
|
||||
)
|
||||
|
||||
val DarkAndroidColorScheme = darkColorScheme(
|
||||
primary = Green80,
|
||||
onPrimary = Green20,
|
||||
primaryContainer = Green30,
|
||||
onPrimaryContainer = Green90,
|
||||
secondary = DarkGreen80,
|
||||
onSecondary = DarkGreen20,
|
||||
secondaryContainer = DarkGreen30,
|
||||
onSecondaryContainer = DarkGreen90,
|
||||
tertiary = Teal80,
|
||||
onTertiary = Teal20,
|
||||
tertiaryContainer = Teal30,
|
||||
onTertiaryContainer = Teal90,
|
||||
error = Red80,
|
||||
onError = Red20,
|
||||
errorContainer = Red30,
|
||||
onErrorContainer = Red90,
|
||||
background = DarkGreenGray10,
|
||||
onBackground = DarkGreenGray90,
|
||||
surface = DarkGreenGray10,
|
||||
onSurface = DarkGreenGray90,
|
||||
surfaceVariant = GreenGray30,
|
||||
onSurfaceVariant = GreenGray80,
|
||||
inverseSurface = DarkGreenGray90,
|
||||
inverseOnSurface = DarkGreenGray10,
|
||||
outline = GreenGray60,
|
||||
)
|
||||
|
||||
val LightAndroidGradientColors = GradientColors(container = DarkGreenGray95)
|
||||
val DarkAndroidGradientColors = GradientColors(container = Color.Black)
|
||||
val LightAndroidBackgroundTheme = BackgroundTheme(color = DarkGreenGray95)
|
||||
val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black)
|
||||
|
||||
@Composable
|
||||
fun OxygenTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
androidTheme: Boolean = false,
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
|
||||
|
||||
dynamicColor && supportsDynamicTheming() -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
|
||||
}
|
||||
|
||||
val emptyGradientColors = GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
|
||||
val defaultGradientColors = GradientColors(
|
||||
top = colorScheme.inverseOnSurface,
|
||||
bottom = colorScheme.primaryContainer,
|
||||
container = colorScheme.surface
|
||||
)
|
||||
val gradientColors = when {
|
||||
androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors
|
||||
dynamicColor && supportsDynamicTheming() -> emptyGradientColors
|
||||
else -> defaultGradientColors
|
||||
}
|
||||
|
||||
val defaultBackgroundTheme = BackgroundTheme(
|
||||
color = colorScheme.surface,
|
||||
tonalElevation = 2.dp
|
||||
)
|
||||
val backgroundTheme = when {
|
||||
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
|
||||
else -> defaultBackgroundTheme
|
||||
}
|
||||
val tintTheme = when {
|
||||
androidTheme -> TintTheme()
|
||||
dynamicColor && supportsDynamicTheming() -> TintTheme(colorScheme.primary)
|
||||
else -> TintTheme()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalGradientColors provides gradientColors,
|
||||
LocalBackgroundTheme provides backgroundTheme,
|
||||
LocalTintTheme provides tintTheme
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = OxygenTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
|
||||
fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
data class TintTheme(
|
||||
val iconTint: Color = Color.Unspecified
|
||||
)
|
||||
|
||||
val LocalTintTheme = staticCompositionLocalOf { TintTheme() }
|
||||
129
app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/theme/Type.kt
Normal file
@@ -0,0 +1,129 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
internal val OxygenTypography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Bottom,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Bottom,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
// Default text style
|
||||
bodyLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
// Used for Button
|
||||
labelLarge = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
),
|
||||
// Used for Navigation items
|
||||
labelMedium = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
),
|
||||
// Used for Tag
|
||||
labelSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 10.sp,
|
||||
lineHeight = 14.sp,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
internal fun ToolsScreen() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.util
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import kotlinx.datetime.TimeZone
|
||||
|
||||
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
|
||||
@@ -0,0 +1,58 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.annotation.RequiresApi
|
||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleUtils {
|
||||
fun switchLocale(activity: Activity, languageConfig: LanguageConfig) {
|
||||
val newLanguage = when (languageConfig) {
|
||||
LanguageConfig.FOLLOW_SYSTEM -> ResourcesUtils.getSystemLocale().get(0)!!.language
|
||||
LanguageConfig.CHINESE -> "zh"
|
||||
LanguageConfig.ENGLISH -> "en"
|
||||
}
|
||||
val currentLanguage = ResourcesUtils.getAppLocale(activity).language
|
||||
if (newLanguage != currentLanguage) {
|
||||
activity.recreate()
|
||||
}
|
||||
}
|
||||
|
||||
fun attachBaseContext(context: Context, languageConfig: LanguageConfig): Context {
|
||||
val locale: Locale = getLocaleFromLanguageConfig(languageConfig)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createConfigurationContext(context, locale)
|
||||
} else {
|
||||
updateConfiguration(context, locale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocaleFromLanguageConfig(languageConfig: LanguageConfig): Locale =
|
||||
when (languageConfig) {
|
||||
LanguageConfig.FOLLOW_SYSTEM -> ResourcesUtils.getSystemLocale().get(0)!!
|
||||
LanguageConfig.CHINESE -> Locale("zh")
|
||||
LanguageConfig.ENGLISH -> Locale("en")
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun createConfigurationContext(context: Context, locale: Locale): Context {
|
||||
val configuration = context.resources.configuration
|
||||
configuration.setLocales(LocaleList(locale))
|
||||
|
||||
return context.createConfigurationContext(configuration)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun updateConfiguration(context: Context, locale: Locale): Context {
|
||||
val resources = context.resources
|
||||
val configuration = resources.configuration
|
||||
configuration.locale = locale
|
||||
resources.updateConfiguration(configuration, resources.displayMetrics)
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.util.Locale
|
||||
|
||||
object ResourcesUtils {
|
||||
fun getConfiguration(context: Context) = context.resources.configuration
|
||||
|
||||
fun getDisplayMetrics(context: Context) = context.resources.displayMetrics
|
||||
|
||||
fun getAppLocale(context: Context): Locale =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) getConfiguration(context).locales.get(0)
|
||||
else getConfiguration(context).locale
|
||||
|
||||
fun getSystemLocale(): LocaleListCompat =
|
||||
ConfigurationCompat.getLocales(Resources.getSystem().configuration)
|
||||
|
||||
fun getAppVersionName(context: Context): String =
|
||||
try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
"Unknown"
|
||||
}
|
||||
|
||||
fun getAppVersionCode(context: Context): Long =
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode
|
||||
else context.packageManager.getPackageInfo(context.packageName, 0).versionCode.toLong()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum DarkThemeConfigProto {
|
||||
DARK_THEME_CONFIG_UNSPECIFIED = 0;
|
||||
DARK_THEME_CONFIG_FOLLOW_SYSTEM = 1;
|
||||
DARK_THEME_CONFIG_LIGHT = 2;
|
||||
DARK_THEME_CONFIG_DARK = 3;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum LanguageConfigProto {
|
||||
LANGUAGE_CONFIG_UNSPECIFIED = 0;
|
||||
LANGUAGE_CONFIG_FOLLOW_SYSTEM = 1;
|
||||
LANGUAGE_CONFIG_CHINESE = 2;
|
||||
LANGUAGE_CONFIG_ENGLISH = 3;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum LaunchPageConfigProto {
|
||||
LAUNCH_PAGE_CONFIG_UNSPECIFIED = 0;
|
||||
LAUNCH_PAGE_CONFIG_TOOLS = 1;
|
||||
LAUNCH_PAGE_CONFIG_STAR = 2;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum ThemeBrandConfigProto {
|
||||
THEME_BRAND_CONFIG_UNSPECIFIED = 0;
|
||||
THEME_BRAND_CONFIG_DEFAULT = 1;
|
||||
THEME_BRAND_CONFIG_ANDROID = 2;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "com/fatweb/oxygen/toolbox/data/language_config.proto";
|
||||
import "com/fatweb/oxygen/toolbox/data/launch_page_config.proto";
|
||||
import "com/fatweb/oxygen/toolbox/data/theme_brand_config.proto";
|
||||
import "com/fatweb/oxygen/toolbox/data/dark_theme_config.proto";
|
||||
|
||||
option java_package = "top.fatweb.oxygen.toolbox.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message UserPreferences {
|
||||
LanguageConfigProto language_config = 1;
|
||||
LaunchPageConfigProto launch_page_config = 2;
|
||||
ThemeBrandConfigProto theme_brand_config = 3;
|
||||
DarkThemeConfigProto dark_theme_config = 4;
|
||||
bool use_dynamic_color = 5;
|
||||
}
|
||||
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
175
app/src/main/res/drawable/ic_oxygen.xml
Normal file
@@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
10
app/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="NightAdjusted.Theme.Oxygen" parent="android:Theme.Material.NoActionBar" />
|
||||
|
||||
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
27
app/src/main/res/values-zh/strings.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">OxygenToolbox</string>
|
||||
<string name="no_connect">⚠️ 无法连接至互联网</string>
|
||||
<string name="feature_tools_title">工具</string>
|
||||
<string name="feature_star_title">收藏</string>
|
||||
<string name="feature_settings_title">设置</string>
|
||||
<string name="feature_settings_loading">加载中…</string>
|
||||
<string name="feature_settings_language">语言</string>
|
||||
<string name="feature_settings_language_system_default">系统默认</string>
|
||||
<string name="feature_settings_launch_page">启动页</string>
|
||||
<string name="feature_settings_launch_page_tools">工具</string>
|
||||
<string name="feature_settings_launch_page_star">收藏</string>
|
||||
<string name="feature_settings_theme_brand">主题类型</string>
|
||||
<string name="feature_settings_theme_brand_default">默认</string>
|
||||
<string name="feature_settings_theme_brand_android">Android</string>
|
||||
<string name="feature_settings_dark_mode">深色模式</string>
|
||||
<string name="feature_settings_dark_mode_system_default">系统默认</string>
|
||||
<string name="feature_settings_dark_mode_light">明亮</string>
|
||||
<string name="feature_settings_dark_mode_dark">深色</string>
|
||||
<string name="feature_settings_dynamic_color">动态颜色</string>
|
||||
<string name="feature_settings_dynamic_color_enable">启用</string>
|
||||
<string name="feature_settings_dynamic_color_disable">禁用</string>
|
||||
<string name="feature_settings_top_app_bar_action_icon_description">更多</string>
|
||||
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
|
||||
<string name="feature_settings_dismiss_dialog_button_text">完成</string>
|
||||
</resources>
|
||||
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
28
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<resources>
|
||||
<string name="app_name">OxygenToolbox</string>
|
||||
<string name="no_connect">⚠️ Unable to connect to the internet</string>
|
||||
<string name="feature_tools_title">Tools</string>
|
||||
<string name="feature_star_title">Star</string>
|
||||
<string name="feature_settings_title">Settings</string>
|
||||
<string name="feature_settings_loading">Loading…</string>
|
||||
<string name="feature_settings_language">Language</string>
|
||||
<string name="feature_settings_language_system_default">System Default</string>
|
||||
<string name="feature_settings_language_chinese" translatable="false">中文</string>
|
||||
<string name="feature_settings_language_english" translatable="false">English</string>
|
||||
<string name="feature_settings_launch_page">Launch Page</string>
|
||||
<string name="feature_settings_launch_page_tools">Tools</string>
|
||||
<string name="feature_settings_launch_page_star">Star</string>
|
||||
<string name="feature_settings_theme_brand">Theme Brand</string>
|
||||
<string name="feature_settings_theme_brand_default">Default</string>
|
||||
<string name="feature_settings_theme_brand_android">Android</string>
|
||||
<string name="feature_settings_dark_mode">Dark Mode</string>
|
||||
<string name="feature_settings_dark_mode_system_default">System Default</string>
|
||||
<string name="feature_settings_dark_mode_light">Light</string>
|
||||
<string name="feature_settings_dark_mode_dark">Dark</string>
|
||||
<string name="feature_settings_dynamic_color">Dynamic Color</string>
|
||||
<string name="feature_settings_dynamic_color_enable">Enable</string>
|
||||
<string name="feature_settings_dynamic_color_disable">Disable</string>
|
||||
<string name="feature_settings_top_app_bar_action_icon_description">More</string>
|
||||
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
|
||||
<string name="feature_settings_dismiss_dialog_button_text">OK</string>
|
||||
</resources>
|
||||
17
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="NightAdjusted.Theme.Oxygen" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
|
||||
<style name="Theme.Oxygen" parent="NightAdjusted.Theme.Oxygen" />
|
||||
|
||||
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Oxygen.Splash" parent="NightAdjusted.Theme.Splash">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_oxygen</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Oxygen</item>
|
||||
</style>
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,17 @@
|
||||
package top.fatweb.oxygen.toolbox
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||