From a0630539e1afab5e7a470707c337c135c6ed7c1a Mon Sep 17 00:00:00 2001 From: hefengbao <754582231@qq.com> Date: Thu, 18 Jul 2024 16:16:49 +0800 Subject: [PATCH] =?UTF-8?q?update:=20=E4=BC=98=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/hefengbao/jingmo/MainActivity.kt | 125 ++++++++++- .../hefengbao/jingmo/MainActivityViewModel.kt | 27 ++- .../jingmo/data/datastore/AppPreference.kt | 29 +++ .../hefengbao/jingmo/data/model/AppStatus.kt | 7 + .../hefengbao/jingmo/data/model/UserData.kt | 1 - .../data/model/theme/DarkThemeConfig.kt | 7 +- .../jingmo/data/model/theme/ThemeBrand.kt | 7 +- .../settings/PreferenceRepository.kt | 1 + .../settings/PreferenceRepositoryImpl.kt | 2 + .../repository/settings/ThemeRepository.kt | 25 +++ .../settings/ThemeRepositoryImpl.kt | 24 +++ .../com/hefengbao/jingmo/di/DataModule.kt | 7 + .../jingmo/ui/screen/home/HomeScreen.kt | 21 ++ .../jingmo/ui/screen/home/HomeViewModel.kt | 18 +- .../ui/screen/settings/SettingsScreen.kt | 14 ++ .../jingmo/ui/screen/settings/ThemeDialog.kt | 200 ++++++++++++++++++ .../screen/settings/ThemeDialogViewModel.kt | 64 ++++++ app/src/main/res/values/strings.xml | 12 ++ 18 files changed, 580 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepository.kt create mode 100644 app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepositoryImpl.kt create mode 100644 app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialog.kt create mode 100644 app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialogViewModel.kt diff --git a/app/src/main/java/com/hefengbao/jingmo/MainActivity.kt b/app/src/main/java/com/hefengbao/jingmo/MainActivity.kt index 4536e901..6de9ea3e 100644 --- a/app/src/main/java/com/hefengbao/jingmo/MainActivity.kt +++ b/app/src/main/java/com/hefengbao/jingmo/MainActivity.kt @@ -10,11 +10,12 @@ package com.hefengbao.jingmo 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.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -22,21 +23,32 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.compose.rememberNavController +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand import com.hefengbao.jingmo.route.AppNavHost import com.hefengbao.jingmo.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) val viewModel: MainActivityViewModel by viewModels() @@ -48,12 +60,63 @@ class MainActivity : ComponentActivity() { // This also sets up the initial system bar style based on the platform theme enableEdgeToEdge() + var uiState: MainActivityUiState by mutableStateOf(MainActivityUiState.Loading) + + // Update the uiState + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState + .onEach { uiState = it } + .collect() + } + } + + // Keep the splash screen on-screen until the UI state is loaded. This condition is + // evaluated each time the app needs to be redrawn so it should be fast to avoid blocking + // the UI. + splashScreen.setKeepOnScreenCondition { + when (uiState) { + MainActivityUiState.Loading -> true + is MainActivityUiState.Success -> false + } + } + + // Turn off the decor fitting system windows, which allows us to handle insets, + // including IME animations, and go edge-to-edge + // This also sets up the initial system bar style based on the platform theme + enableEdgeToEdge() + setContent { + val darkTheme = shouldUseDarkTheme(uiState) + + // Update the edge to edge configuration to match the theme + // This is the same parameters as the default enableEdgeToEdge call, but we manually + // resolve whether or not to show dark theme using uiState, since it can be different + // than the configuration's dark theme value based on the user preference. + DisposableEffect(darkTheme) { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + android.graphics.Color.TRANSPARENT, + android.graphics.Color.TRANSPARENT, + ) { darkTheme }, + navigationBarStyle = SystemBarStyle.auto( + lightScrim, + darkScrim, + ) { darkTheme }, + ) + onDispose {} + } + val appNavController = rememberNavController() + val showLanding by viewModel.showLanding.collectAsState(initial = true) - AppTheme { + AppTheme( + darkTheme = darkTheme, + androidTheme = shouldUseAndroidTheme(uiState), + disableDynamicTheming = shouldDisableDynamicTheming(uiState), + ) { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), @@ -69,7 +132,6 @@ class MainActivity : ComponentActivity() { } destination?.let { - Log.i("MainActivity", "destination") appNavController.navigate(it) } } @@ -92,4 +154,57 @@ private fun LandingScreen( .align(Alignment.Center), ) } -} \ No newline at end of file +} + +/** + * Returns `true` if the Android theme should be used, as a function of the [uiState]. + */ +@Composable +private fun shouldUseAndroidTheme( + uiState: MainActivityUiState, +): Boolean = when (uiState) { + MainActivityUiState.Loading -> false + is MainActivityUiState.Success -> when (uiState.userData.themeBrand) { + ThemeBrand.DEFAULT -> false + ThemeBrand.ANDROID -> true + } +} + +/** + * Returns `true` if the dynamic color is disabled, as a function of the [uiState]. + */ +@Composable +private fun shouldDisableDynamicTheming( + uiState: MainActivityUiState, +): Boolean = when (uiState) { + MainActivityUiState.Loading -> false + is MainActivityUiState.Success -> !uiState.userData.useDynamicColor +} + +/** + * Returns `true` if dark theme should be used, as a function of the [uiState] and the + * current system context. + */ +@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 + } +} + +/** + * The default light scrim, as defined by androidx and the platform: + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598 + */ +private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF) + +/** + * The default dark scrim, as defined by androidx and the platform: + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598 + */ +private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b) diff --git a/app/src/main/java/com/hefengbao/jingmo/MainActivityViewModel.kt b/app/src/main/java/com/hefengbao/jingmo/MainActivityViewModel.kt index 6172855a..0bb30c2c 100644 --- a/app/src/main/java/com/hefengbao/jingmo/MainActivityViewModel.kt +++ b/app/src/main/java/com/hefengbao/jingmo/MainActivityViewModel.kt @@ -11,12 +11,17 @@ package com.hefengbao.jingmo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.hefengbao.jingmo.data.model.UserData import com.hefengbao.jingmo.data.repository.settings.PreferenceRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -33,13 +38,31 @@ class MainActivityViewModel @Inject constructor( } } - private val _showLanding: MutableStateFlow = MutableStateFlow(true) - var showLanding: SharedFlow = _showLanding + val uiState: StateFlow = preferenceRepository.getAppStatus().map { + MainActivityUiState.Success( + userData = UserData( + themeBrand = it.themeBrand, + darkThemeConfig = it.darkThemeConfig, + useDynamicColor = it.useDynamicColor, + ) + ) + }.stateIn( + scope = viewModelScope, + initialValue = MainActivityUiState.Loading, + started = SharingStarted.WhileSubscribed(5_000), + ) + private val _showLanding: MutableStateFlow = MutableStateFlow(true) + val showLanding: SharedFlow = _showLanding fun closeLanding() { viewModelScope.launch { delay(1500) _showLanding.value = false } } +} + +sealed interface MainActivityUiState { + data object Loading : MainActivityUiState + data class Success(val userData: UserData) : MainActivityUiState } \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/datastore/AppPreference.kt b/app/src/main/java/com/hefengbao/jingmo/data/datastore/AppPreference.kt index ea5a82c6..843710dd 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/datastore/AppPreference.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/datastore/AppPreference.kt @@ -12,11 +12,14 @@ package com.hefengbao.jingmo.data.datastore import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.hefengbao.jingmo.common.Constant import com.hefengbao.jingmo.data.model.AppStatus +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -40,6 +43,10 @@ private suspend fun setString(context: Context, key: Preferences.Key, va context.app.edit { it[key] = value } } +private suspend fun setBoolean(context: Context, key: Preferences.Key, value: Boolean) { + context.app.edit { it[key] = value } +} + class AppPreference( private val context: Context ) { @@ -47,6 +54,12 @@ class AppPreference( AppStatus( captureTextColor = it[PREF_CAPTURE_TEXT_COLOR] ?: "white", captureBackgroundColor = it[PREF_CAPTURE_BACKGROUND_COLOR] ?: "#065279", + themeBrand = ThemeBrand.from(it[PREF_THEME_BRAND] ?: ThemeBrand.DEFAULT.name), + darkThemeConfig = DarkThemeConfig.from( + it[PREF_DARK_THEME_CONFIG] ?: DarkThemeConfig.FOLLOW_SYSTEM.name + ), + useDynamicColor = it[PREF_USE_DYNAMIC_COLOR] ?: false, + showSyncDataTip = it[PREF_SHOW_SYNC_DATA_TIP] ?: true ) } @@ -56,9 +69,25 @@ class AppPreference( suspend fun setCaptureBackgroundColor(color: String) = setString(context, PREF_CAPTURE_BACKGROUND_COLOR, color) + suspend fun setThemeBrand(brand: ThemeBrand) = + setString(context, PREF_THEME_BRAND, brand.name) + + suspend fun setDarkThemeConfig(config: DarkThemeConfig) = + setString(context, PREF_DARK_THEME_CONFIG, config.name) + + suspend fun setUseDynamicColor(useDynamicColor: Boolean) = + setBoolean(context, PREF_USE_DYNAMIC_COLOR, useDynamicColor) + + suspend fun setShowSyncDataTip(show: Boolean) = + setBoolean(context, PREF_SHOW_SYNC_DATA_TIP, show) + companion object { private val PREF_CAPTURE_TEXT_COLOR = stringPreferencesKey("key_capture_text_color") private val PREF_CAPTURE_BACKGROUND_COLOR = stringPreferencesKey("key_capture_background_color") + private val PREF_THEME_BRAND = stringPreferencesKey("key_theme_brand") + private val PREF_DARK_THEME_CONFIG = stringPreferencesKey("key_dark_theme_config") + private val PREF_USE_DYNAMIC_COLOR = booleanPreferencesKey("key_use_dynamic_color") + private val PREF_SHOW_SYNC_DATA_TIP = booleanPreferencesKey("key_show_sync_data_tip") } } \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/model/AppStatus.kt b/app/src/main/java/com/hefengbao/jingmo/data/model/AppStatus.kt index 2a416613..5e08a0f5 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/model/AppStatus.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/model/AppStatus.kt @@ -9,7 +9,14 @@ package com.hefengbao.jingmo.data.model +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand + data class AppStatus( val captureTextColor: String, val captureBackgroundColor: String, + val themeBrand: ThemeBrand, + val darkThemeConfig: DarkThemeConfig, + val useDynamicColor: Boolean, + val showSyncDataTip: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/model/UserData.kt b/app/src/main/java/com/hefengbao/jingmo/data/model/UserData.kt index cbf496f8..bfcb604b 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/model/UserData.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/model/UserData.kt @@ -16,5 +16,4 @@ data class UserData( val themeBrand: ThemeBrand, val darkThemeConfig: DarkThemeConfig, val useDynamicColor: Boolean, - val shouldHideOnboarding: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/model/theme/DarkThemeConfig.kt b/app/src/main/java/com/hefengbao/jingmo/data/model/theme/DarkThemeConfig.kt index 61f6fc5c..44ba4c07 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/model/theme/DarkThemeConfig.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/model/theme/DarkThemeConfig.kt @@ -12,5 +12,10 @@ package com.hefengbao.jingmo.data.model.theme enum class DarkThemeConfig { FOLLOW_SYSTEM, LIGHT, - DARK, + DARK; + + companion object { + infix fun from(value: String): DarkThemeConfig = + DarkThemeConfig.entries.firstOrNull { it.name == value } ?: FOLLOW_SYSTEM + } } diff --git a/app/src/main/java/com/hefengbao/jingmo/data/model/theme/ThemeBrand.kt b/app/src/main/java/com/hefengbao/jingmo/data/model/theme/ThemeBrand.kt index 9a3a754a..c90e1b3f 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/model/theme/ThemeBrand.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/model/theme/ThemeBrand.kt @@ -11,5 +11,10 @@ package com.hefengbao.jingmo.data.model.theme enum class ThemeBrand { DEFAULT, - ANDROID, + ANDROID; + + companion object { + infix fun from(value: String): ThemeBrand = + ThemeBrand.entries.firstOrNull { it.name == value } ?: DEFAULT + } } diff --git a/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepository.kt b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepository.kt index a72c09dd..d60758e0 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepository.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepository.kt @@ -46,4 +46,5 @@ interface PreferenceRepository { fun getAppStatus(): Flow suspend fun setCaptureTextColor(color: String) suspend fun setCaptureBackgroundColor(color: String) + suspend fun setShowSyncDataTip(show: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepositoryImpl.kt b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepositoryImpl.kt index b38f765e..8d4fb973 100644 --- a/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepositoryImpl.kt +++ b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/PreferenceRepositoryImpl.kt @@ -103,4 +103,6 @@ class PreferenceRepositoryImpl @Inject constructor( override suspend fun setCaptureBackgroundColor(color: String) = app.setCaptureBackgroundColor(color) + + override suspend fun setShowSyncDataTip(show: Boolean) = app.setShowSyncDataTip(show) } \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepository.kt b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepository.kt new file mode 100644 index 00000000..e1559ea7 --- /dev/null +++ b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepository.kt @@ -0,0 +1,25 @@ +package com.hefengbao.jingmo.data.repository.settings + +import com.hefengbao.jingmo.data.model.AppStatus +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand +import kotlinx.coroutines.flow.Flow + +interface ThemeRepository { + val appStatus: Flow + + /** + * Sets the desired theme brand. + */ + suspend fun setThemeBrand(themeBrand: ThemeBrand) + + /** + * Sets the desired dark theme config. + */ + suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) + + /** + * Sets the preferred dynamic color config. + */ + suspend fun setDynamicColorPreference(useDynamicColor: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepositoryImpl.kt b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepositoryImpl.kt new file mode 100644 index 00000000..a8788eb9 --- /dev/null +++ b/app/src/main/java/com/hefengbao/jingmo/data/repository/settings/ThemeRepositoryImpl.kt @@ -0,0 +1,24 @@ +package com.hefengbao.jingmo.data.repository.settings + +import com.hefengbao.jingmo.data.datastore.AppPreference +import com.hefengbao.jingmo.data.model.AppStatus +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class ThemeRepositoryImpl @Inject constructor( + private val preference: AppPreference +) : ThemeRepository { + override val appStatus: Flow + get() = preference.appStatus + + override suspend fun setThemeBrand(themeBrand: ThemeBrand) = + preference.setThemeBrand(themeBrand) + + override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) = + preference.setDarkThemeConfig(darkThemeConfig) + + override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) = + preference.setUseDynamicColor(useDynamicColor) +} \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/di/DataModule.kt b/app/src/main/java/com/hefengbao/jingmo/di/DataModule.kt index 4f52c5a0..4ba41d38 100644 --- a/app/src/main/java/com/hefengbao/jingmo/di/DataModule.kt +++ b/app/src/main/java/com/hefengbao/jingmo/di/DataModule.kt @@ -45,6 +45,8 @@ import com.hefengbao.jingmo.data.repository.settings.NetworkDatasourceRepository import com.hefengbao.jingmo.data.repository.settings.NetworkDatasourceRepositoryImpl import com.hefengbao.jingmo.data.repository.settings.PreferenceRepository import com.hefengbao.jingmo.data.repository.settings.PreferenceRepositoryImpl +import com.hefengbao.jingmo.data.repository.settings.ThemeRepository +import com.hefengbao.jingmo.data.repository.settings.ThemeRepositoryImpl import com.hefengbao.jingmo.data.repository.traditionalculture.ColorRepository import com.hefengbao.jingmo.data.repository.traditionalculture.ColorRepositoryImpl import com.hefengbao.jingmo.data.repository.traditionalculture.FestivalRepository @@ -163,4 +165,9 @@ interface DataModule { fun bindsChineseProverbRepository( proverbRepository: ProverbRepositoryImpl ): ProverbRepository + + @Binds + fun bindsThemeRepository( + themeRepositoryImpl: ThemeRepositoryImpl + ): ThemeRepository } \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeScreen.kt b/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeScreen.kt index 343dcb37..0ac529ae 100644 --- a/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -30,6 +31,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -68,6 +70,7 @@ fun HomeRoute( onSettingsClick: () -> Unit, ) { val homeItem by viewModel.homeItem.collectAsState(initial = HomeItem()) + val showSyncDataTip by viewModel.showSyncDataTip.collectAsState(initial = false) HomeScreen( homeItem = homeItem, @@ -89,6 +92,8 @@ fun HomeRoute( onTraditionalCultureSolarTermsClick = onTraditionalCultureSolarTermsClick, onLinksClick = onLinksClick, onSettingsClick = onSettingsClick, + showSyncDataTip = showSyncDataTip, + updateShowSyncDataTip = { viewModel.updateShowSyncDataTip() } ) } @@ -115,7 +120,23 @@ private fun HomeScreen( onTraditionalCultureSolarTermsClick: () -> Unit, onLinksClick: () -> Unit, onSettingsClick: () -> Unit, + showSyncDataTip: Boolean, + updateShowSyncDataTip: () -> Unit, ) { + if (showSyncDataTip) { + AlertDialog( + onDismissRequest = updateShowSyncDataTip, + confirmButton = { + TextButton(onClick = updateShowSyncDataTip) { + Text(text = "知道了") + } + }, + text = { + Text(text = "如果首次使用,请点击右上角设置(⚙)同步数据或者导入数据") + } + ) + } + Scaffold( topBar = { TopAppBar( diff --git a/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeViewModel.kt b/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeViewModel.kt index a5a9cfd2..3cc74740 100644 --- a/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeViewModel.kt +++ b/app/src/main/java/com/hefengbao/jingmo/ui/screen/home/HomeViewModel.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.hefengbao.jingmo.data.model.HomeItem import com.hefengbao.jingmo.data.repository.settings.HomeItemRepository +import com.hefengbao.jingmo.data.repository.settings.PreferenceRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -22,14 +23,29 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - homeRepository: HomeItemRepository + homeRepository: HomeItemRepository, + private val preferenceRepository: PreferenceRepository ) : ViewModel() { private val _homeItem: MutableStateFlow = MutableStateFlow(HomeItem()) val homeItem: SharedFlow = _homeItem + private val _showSyncDataTip: MutableStateFlow = MutableStateFlow(false) + val showSyncDataTip: SharedFlow = _showSyncDataTip + init { viewModelScope.launch { homeRepository.getHomeItem().collectLatest { _homeItem.value = it } } + viewModelScope.launch { + preferenceRepository.getAppStatus().collectLatest { + _showSyncDataTip.value = it.showSyncDataTip + } + } + } + + fun updateShowSyncDataTip() { + viewModelScope.launch { + preferenceRepository.setShowSyncDataTip(false) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/SettingsScreen.kt index 3f82a0d1..76be96e4 100644 --- a/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/SettingsScreen.kt @@ -29,6 +29,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -64,6 +68,14 @@ fun SettingsScreen( onImportClick: () -> Unit, onPrivacyClick: () -> Unit, ) { + var showThemeDialog by rememberSaveable { mutableStateOf(false) } + + if (showThemeDialog) { + ThemeDialog { + showThemeDialog = false + } + } + Scaffold( topBar = { TopAppBar( @@ -87,6 +99,8 @@ fun SettingsScreen( .fillMaxWidth() .verticalScroll(rememberScrollState()), ) { + SettingsTitle(title = "UI") + Item(title = "主题设置", onClick = { showThemeDialog = true }, showBadge = false) SettingsTitle(title = "功能") Item(title = "同步数据", onClick = onDataClick, showBadge = false) Item(title = "导入数据", onClick = onImportClick, showBadge = false) diff --git a/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialog.kt b/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialog.kt new file mode 100644 index 00000000..4329a4f7 --- /dev/null +++ b/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialog.kt @@ -0,0 +1,200 @@ +package com.hefengbao.jingmo.ui.screen.settings + +import androidx.compose.animation.AnimatedVisibility +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.fillMaxWidth +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.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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 com.hefengbao.jingmo.R.string +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand +import com.hefengbao.jingmo.ui.theme.supportsDynamicTheming + +@Composable +fun ThemeDialog( + viewModel: ThemeDialogViewModel = hiltViewModel(), + onDismiss: () -> Unit +) { + val settingsUiState by viewModel.settingsUiState.collectAsState() + ThemeDialog( + onDismiss = onDismiss, + settingsUiState = settingsUiState, + onChangeThemeBrand = viewModel::updateThemeBrand, + onChangeDynamicColorPreference = viewModel::updateDynamicColorPreference, + onChangeDarkThemeConfig = viewModel::updateDarkThemeConfig, + ) +} + +@Composable +fun ThemeDialog( + settingsUiState: SettingsUiState, + supportDynamicColor: Boolean = supportsDynamicTheming(), + onDismiss: () -> Unit, + onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit, + onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit, + onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit, +) { + val configuration = LocalConfiguration.current + + AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + modifier = Modifier.widthIn(max = configuration.screenWidthDp.dp - 80.dp), + onDismissRequest = onDismiss, + title = { + Text( + text = "主题设置", + style = MaterialTheme.typography.titleLarge, + ) + }, + text = { + Column(Modifier.verticalScroll(rememberScrollState())) { + when (settingsUiState) { + SettingsUiState.Loading -> { + Text( + text = stringResource(string.feature_settings_loading), + modifier = Modifier.padding(vertical = 16.dp), + ) + } + + is SettingsUiState.Success -> { + SettingsPanel( + settings = settingsUiState.settings, + supportDynamicColor = supportDynamicColor, + onChangeThemeBrand = onChangeThemeBrand, + onChangeDynamicColorPreference = onChangeDynamicColorPreference, + onChangeDarkThemeConfig = onChangeDarkThemeConfig, + ) + } + } + } + }, + confirmButton = { + TextButton( + onClick = onDismiss + ) { + Text( + text = stringResource(string.feature_settings_dismiss_dialog_button_text), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + } + ) +} + +@Composable +private fun ColumnScope.SettingsPanel( + settings: UserEditableSettings, + supportDynamicColor: Boolean, + onChangeThemeBrand: (themeBrand: ThemeBrand) -> Unit, + onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit, + onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit, +) { + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_theme)) + Column(Modifier.selectableGroup()) { + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_brand_default), + selected = settings.brand == ThemeBrand.DEFAULT, + onClick = { onChangeThemeBrand(ThemeBrand.DEFAULT) }, + ) + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_brand_android), + selected = settings.brand == ThemeBrand.ANDROID, + onClick = { onChangeThemeBrand(ThemeBrand.ANDROID) }, + ) + } + AnimatedVisibility(visible = settings.brand == ThemeBrand.DEFAULT && supportDynamicColor) { + Column { + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_dynamic_color_preference)) + Column(Modifier.selectableGroup()) { + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_dynamic_color_yes), + selected = settings.useDynamicColor, + onClick = { onChangeDynamicColorPreference(true) }, + ) + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_dynamic_color_no), + selected = !settings.useDynamicColor, + onClick = { onChangeDynamicColorPreference(false) }, + ) + } + } + } + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_dark_mode_preference)) + Column(Modifier.selectableGroup()) { + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_dark_mode_config_system_default), + selected = settings.darkThemeConfig == DarkThemeConfig.FOLLOW_SYSTEM, + onClick = { onChangeDarkThemeConfig(DarkThemeConfig.FOLLOW_SYSTEM) }, + ) + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_dark_mode_config_light), + selected = settings.darkThemeConfig == DarkThemeConfig.LIGHT, + onClick = { onChangeDarkThemeConfig(DarkThemeConfig.LIGHT) }, + ) + SettingsDialogThemeChooserRow( + text = stringResource(string.feature_settings_dark_mode_config_dark), + selected = settings.darkThemeConfig == DarkThemeConfig.DARK, + onClick = { onChangeDarkThemeConfig(DarkThemeConfig.DARK) }, + ) + } +} + +@Composable +private fun SettingsDialogSectionTitle(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + ) +} + +@Composable +fun SettingsDialogThemeChooserRow( + text: String, + selected: Boolean, + onClick: () -> Unit, +) { + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = onClick, + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = selected, + onClick = null, + ) + Spacer(Modifier.width(8.dp)) + Text(text) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialogViewModel.kt b/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialogViewModel.kt new file mode 100644 index 00000000..c783bc2b --- /dev/null +++ b/app/src/main/java/com/hefengbao/jingmo/ui/screen/settings/ThemeDialogViewModel.kt @@ -0,0 +1,64 @@ +package com.hefengbao.jingmo.ui.screen.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hefengbao.jingmo.data.model.theme.DarkThemeConfig +import com.hefengbao.jingmo.data.model.theme.ThemeBrand +import com.hefengbao.jingmo.data.repository.settings.ThemeRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +@HiltViewModel +class ThemeDialogViewModel @Inject constructor( + private val repository: ThemeRepository +) : ViewModel() { + val settingsUiState: StateFlow = + repository.appStatus.map { status -> + SettingsUiState.Success( + settings = UserEditableSettings( + brand = status.themeBrand, + darkThemeConfig = status.darkThemeConfig, + useDynamicColor = status.useDynamicColor + ) + ) + }.stateIn( + scope = viewModelScope, + started = WhileSubscribed(5.seconds.inWholeMilliseconds), + initialValue = SettingsUiState.Loading, + ) + + fun updateThemeBrand(themeBrand: ThemeBrand) { + viewModelScope.launch { + repository.setThemeBrand(themeBrand) + } + } + + fun updateDarkThemeConfig(darkThemeConfig: DarkThemeConfig) { + viewModelScope.launch { + repository.setDarkThemeConfig(darkThemeConfig) + } + } + + fun updateDynamicColorPreference(useDynamicColor: Boolean) { + viewModelScope.launch { + repository.setDynamicColorPreference(useDynamicColor) + } + } +} + +data class UserEditableSettings( + val brand: ThemeBrand, + val useDynamicColor: Boolean, + val darkThemeConfig: DarkThemeConfig, +) + +sealed interface SettingsUiState { + data object Loading : SettingsUiState + data class Success(val settings: UserEditableSettings) : SettingsUiState +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33e9832d..76a3687a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,16 @@ 成语 诗文 诗文名句 + Loading… + 主题 + 默认 + Android + 深色模式 + 跟随系统 + 浅色 + 深色 + 使用动态颜色 + + + 确认 \ No newline at end of file