diff --git a/src/main/kotlin/state/AppState.kt b/src/main/kotlin/state/AppState.kt index 5f4a47c..e1902b9 100644 --- a/src/main/kotlin/state/AppState.kt +++ b/src/main/kotlin/state/AppState.kt @@ -40,7 +40,7 @@ class AppState { var global: GlobalState = loadGlobalState() /** Material 颜色 */ - var colors by mutableStateOf(createColors(global.isDarkTheme, global.primaryColor,global.backgroundColor,global.onBackgroundColor)) + var colors by mutableStateOf(createColors(global)) /** 视频播放窗口,使用 JFrame 的一个原因是 swingPanel 重组的时候会产生闪光, * 相关 Issue: https://github.com/JetBrains/compose-jb/issues/1800, @@ -51,7 +51,7 @@ class AppState { var videoPlayerComponent = createMediaPlayerComponent() /** 文件选择器,如果不提前加载反应会很慢 */ - var futureFileChooser: FutureTask = initializeFileChooser(global.isDarkTheme) + var futureFileChooser: FutureTask = initializeFileChooser(global.isDarkTheme,global.isFollowSystemTheme) /** 困难词库 */ var hardVocabulary = loadMutableVocabularyByName("HardVocabulary") @@ -142,6 +142,7 @@ class AppState { val globalData = GlobalData( global.type, global.isDarkTheme, + global.isFollowSystemTheme, global.audioVolume, global.videoVolume, global.keystrokeVolume, diff --git a/src/main/kotlin/state/GlobalState.kt b/src/main/kotlin/state/GlobalState.kt index a41f51c..e0b78a5 100644 --- a/src/main/kotlin/state/GlobalState.kt +++ b/src/main/kotlin/state/GlobalState.kt @@ -18,6 +18,7 @@ import kotlinx.serialization.Serializable data class GlobalData( val type: ScreenType = ScreenType.WORD, val isDarkTheme: Boolean = true, + val isFollowSystemTheme: Boolean = false, val audioVolume: Float = 0.8F, val videoVolume: Float = 80F, val keystrokeVolume: Float = 0.75F, @@ -53,6 +54,11 @@ class GlobalState(globalData: GlobalData) { */ var isDarkTheme by mutableStateOf(globalData.isDarkTheme) + /** + * 是否跟随系统主题 + */ + var isFollowSystemTheme by mutableStateOf(globalData.isFollowSystemTheme) + /** * 单词发音的音量 */ diff --git a/src/main/kotlin/theme/colors.kt b/src/main/kotlin/theme/colors.kt index aa4364e..f56e392 100644 --- a/src/main/kotlin/theme/colors.kt +++ b/src/main/kotlin/theme/colors.kt @@ -4,16 +4,24 @@ import androidx.compose.material.Colors import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.ui.graphics.Color +import org.slf4j.LoggerFactory +import state.GlobalState val IDEADarkThemeOnBackground = Color(133, 144, 151) + fun createColors( isDarkTheme: Boolean, + isFollowSystemTheme:Boolean = true, primary: Color, background:Color, onBackground:Color ): Colors { - return if (isDarkTheme) { + val isDark = if (isFollowSystemTheme) { + isSystemDarkMode() + } else isDarkTheme + + return if (isDark) { darkColors( primary = primary, onBackground = IDEADarkThemeOnBackground @@ -28,10 +36,81 @@ fun createColors( } } + +fun createColors( + global: GlobalState +): Colors { + val isDark = if (global.isFollowSystemTheme) { + isSystemDarkMode() + } else global.isDarkTheme + + return if (isDark) { + darkColors( + primary = global.primaryColor, + onBackground = IDEADarkThemeOnBackground + ) + } else { + lightColors( + primary = global.primaryColor, + background = global.backgroundColor, + surface = global.backgroundColor, + onBackground = global.onBackgroundColor + ) + } +} + fun java.awt.Color.toCompose(): Color { return Color(red, green, blue) } fun Color.toAwt(): java.awt.Color { return java.awt.Color(red, green, blue) +} + +fun isSystemDarkMode(): Boolean { + val logger = LoggerFactory.getLogger("isSystemDarkMode") + return when { + System.getProperty("os.name").contains("Mac", ignoreCase = true) -> { + val command = arrayOf("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle") + try { + val process = Runtime.getRuntime().exec(command) + process.inputStream.bufferedReader().use { it.readText().trim() == "Dark" } + } catch (e: Exception) { + logError(e, logger) + false + } + } + System.getProperty("os.name").contains("Windows", ignoreCase = true) -> { + val command = "reg query HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize /v AppsUseLightTheme" + try { + val process = Runtime.getRuntime().exec(command) + process.inputStream.bufferedReader().use { reader -> + val output = reader.readText() + !output.contains("0x1") + } + } catch (e: Exception) { + logError(e, logger) + false + } + } + System.getProperty("os.name").contains("Linux", ignoreCase = true) -> { + // 还没有在Linux上测试 + val command = arrayOf("gsettings", "get", "org.gnome.desktop.interface", "gtk-theme") + try { + val process = Runtime.getRuntime().exec(command) + process.inputStream.bufferedReader().use { reader -> + val output = reader.readText().trim() + output.contains("dark", ignoreCase = true) + } + } catch (e: Exception) { + logError(e, logger) + false + } + } + else -> false + } +} + +fun logError(e: Exception, logger: org.slf4j.Logger) { + logger.error("Error StackTrace: ${e.stackTraceToString()}\n") } \ No newline at end of file diff --git a/src/main/kotlin/ui/App.kt b/src/main/kotlin/ui/App.kt index a1231dd..6f0ee52 100644 --- a/src/main/kotlin/ui/App.kt +++ b/src/main/kotlin/ui/App.kt @@ -308,7 +308,12 @@ fun App( vocabularyPath = chosenPath, isDarkTheme = appState.global.isDarkTheme, updateFlatLaf = { - updateFlatLaf(appState.global.isDarkTheme,appState.global.backgroundColor.toAwt(),appState.global.onBackgroundColor.toAwt()) + updateFlatLaf( + darkTheme = appState.global.isDarkTheme, + isFollowSystemTheme = appState.global.isFollowSystemTheme, + background = appState.global.backgroundColor.toAwt(), + onBackground = appState.global.onBackgroundColor.toAwt() + ) } ) }else{ @@ -318,8 +323,13 @@ fun App( } // 改变主题后,更新菜单栏、标题栏的样式 - LaunchedEffect(appState.global.isDarkTheme){ - updateFlatLaf(appState.global.isDarkTheme,appState.global.backgroundColor.toAwt(),appState.global.onBackgroundColor.toAwt()) + LaunchedEffect(appState.global.isDarkTheme,appState.global.isFollowSystemTheme){ + updateFlatLaf( + darkTheme = appState.global.isDarkTheme, + isFollowSystemTheme = appState.global.isFollowSystemTheme, + background = appState.global.backgroundColor.toAwt(), + onBackground = appState.global.onBackgroundColor.toAwt() + ) appState.futureFileChooser = setupFileChooser() } } diff --git a/src/main/kotlin/ui/dialog/SettingsDialog.kt b/src/main/kotlin/ui/dialog/SettingsDialog.kt index d5d63a1..67dab73 100644 --- a/src/main/kotlin/ui/dialog/SettingsDialog.kt +++ b/src/main/kotlin/ui/dialog/SettingsDialog.kt @@ -3,11 +3,12 @@ package ui.dialog import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.VolumeDown +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,6 +29,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import state.AppState import theme.createColors +import theme.isSystemDarkMode import theme.toAwt import ui.flatlaf.updateFlatLaf import ui.window.windowBackgroundFlashingOnCloseFixHack @@ -499,32 +501,97 @@ fun SettingTheme( Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(top = 20.dp) + modifier = Modifier.fillMaxWidth().padding(top = 20.dp,bottom = 10.dp) ) { - Text("深色模式", color = MaterialTheme.colors.onBackground, modifier = Modifier.padding(start = 10.dp)) - Spacer(Modifier.width(15.dp)) - Switch( - colors = SwitchDefaults.colors(checkedThumbColor = MaterialTheme.colors.primary), - checked = appState.global.isDarkTheme, - onCheckedChange = { - scope.launch { - appState.global.isDarkTheme = it - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) - appState.saveGlobalState() + val width = 120.dp + Column ( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.width(width).padding(end = 10.dp) + .clickable { + scope.launch { + if(appState.global.isFollowSystemTheme || !appState.global.isDarkTheme){ + appState.global.isDarkTheme = true + appState.global.isFollowSystemTheme = false + appState.colors = createColors(appState.global) + appState.saveGlobalState() + } + } } + ){ + val tint = if(!appState.global.isFollowSystemTheme && appState.global.isDarkTheme) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground + Icon( + Icons.Outlined.DarkMode, + contentDescription = "Localized description", + modifier = Modifier.size(60.dp, 60.dp), + tint = tint + ) + Text( + text = "深色模式", fontSize = 12.sp, + ) + } + Column ( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.width(width).padding(end = 10.dp) + .clickable { + scope.launch { + if(appState.global.isFollowSystemTheme || appState.global.isDarkTheme){ + appState.global.isDarkTheme = false + appState.global.isFollowSystemTheme = false + appState.colors = createColors(appState.global) + appState.saveGlobalState() + } + } - }, - ) - Spacer(Modifier.width(80.dp)) + } + ){ + val tint = if(!appState.global.isFollowSystemTheme && !appState.global.isDarkTheme) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground + Icon( + Icons.Outlined.LightMode, + contentDescription = "Localized description", + modifier = Modifier.size(60.dp, 60.dp), + tint = tint + ) + Text( + text = "浅色模式", fontSize = 12.sp, + ) + } + Column ( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.width(width).padding(end = 10.dp) + .clickable { + scope.launch { + if(!appState.global.isFollowSystemTheme){ + appState.global.isFollowSystemTheme = true + appState.colors = createColors(appState.global) + appState.saveGlobalState() + } + } + } + ){ + val imageVector = if(appState.global.isFollowSystemTheme){ + val isDark = isSystemInDarkTheme() + if(isDark) Icons.Outlined.Brightness4 else Icons.Outlined.Brightness7 + } else { + Icons.Outlined.BrightnessAuto + } + Icon( + imageVector, + contentDescription = "Localized description", + modifier = Modifier.size(60.dp, 60.dp), + tint = if(appState.global.isFollowSystemTheme) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground + ) + Text( + text = "跟随系统", fontSize = 12.sp, + ) + } + Spacer(Modifier.width(90.dp)) } var selectPrimaryColor by remember { mutableStateOf(false) } - OutlinedButton(onClick = { selectPrimaryColor = true }, Modifier.padding(end = 80.dp)) { + OutlinedButton(onClick = { selectPrimaryColor = true }, Modifier.padding(end = 100.dp)) { Text("主色调") } if(selectPrimaryColor){ @@ -534,24 +601,14 @@ fun SettingTheme( initColor = appState.global.primaryColor, confirm = { selectedColor -> appState.global.primaryColor = selectedColor - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) + appState.colors = createColors(appState.global) appState.saveGlobalState() selectPrimaryColor = false }, reset = { // 恢复默认颜色,绿色 appState.global.primaryColor = Color(9, 175, 0) - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) + appState.colors = createColors(appState.global) appState.saveGlobalState() selectPrimaryColor = false }, @@ -559,11 +616,14 @@ fun SettingTheme( ) } - if (!appState.global.isDarkTheme) { + val isDark = if (appState.global.isFollowSystemTheme) { + isSystemDarkMode() + } else appState.global.isDarkTheme + if (!isDark) { var selectBackgroundColor by remember { mutableStateOf(false) } var selectOnBackgroundColor by remember { mutableStateOf(false) } - OutlinedButton(onClick = { selectBackgroundColor = true }, Modifier.padding(end = 80.dp)) { + OutlinedButton(onClick = { selectBackgroundColor = true }, Modifier.padding(end = 100.dp)) { Text("设置背景颜色") } if(selectBackgroundColor){ @@ -573,32 +633,24 @@ fun SettingTheme( initColor = appState.global.backgroundColor, confirm = { selectedColor -> appState.global.backgroundColor = selectedColor - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) + appState.colors = createColors(appState.global) updateFlatLaf( - appState.global.isDarkTheme, - appState.global.backgroundColor.toAwt(), - appState.global.onBackgroundColor.toAwt() + darkTheme = appState.global.isDarkTheme, + isFollowSystemTheme = appState.global.isFollowSystemTheme, + background = appState.global.backgroundColor.toAwt(), + onBackground = appState.global.onBackgroundColor.toAwt() ) appState.saveGlobalState() selectBackgroundColor = false }, reset = { appState.global.backgroundColor = Color.White - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) + appState.colors = createColors(appState.global) updateFlatLaf( - appState.global.isDarkTheme, - appState.global.backgroundColor.toAwt(), - appState.global.onBackgroundColor.toAwt() + darkTheme = appState.global.isDarkTheme, + isFollowSystemTheme = appState.global.isFollowSystemTheme, + background = appState.global.backgroundColor.toAwt(), + onBackground = appState.global.onBackgroundColor.toAwt() ) appState.saveGlobalState() selectBackgroundColor = false @@ -606,7 +658,7 @@ fun SettingTheme( appState = appState ) } - OutlinedButton(onClick = { selectOnBackgroundColor = true }, Modifier.padding(end = 80.dp)) { + OutlinedButton(onClick = { selectOnBackgroundColor = true }, Modifier.padding(end = 90.dp)) { Text("设置前景颜色") } @@ -617,16 +669,12 @@ fun SettingTheme( initColor = appState.global.onBackgroundColor, confirm = { selectedColor -> appState.global.onBackgroundColor = selectedColor - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) + appState.colors = createColors(appState.global) updateFlatLaf( - appState.global.isDarkTheme, - appState.global.backgroundColor.toAwt(), - appState.global.onBackgroundColor.toAwt() + darkTheme = appState.global.isDarkTheme, + isFollowSystemTheme = appState.global.isFollowSystemTheme, + background = appState.global.backgroundColor.toAwt(), + onBackground = appState.global.onBackgroundColor.toAwt() ) appState.saveGlobalState() selectOnBackgroundColor = false @@ -634,16 +682,12 @@ fun SettingTheme( }, reset = { appState.global.onBackgroundColor = Color.Black - appState.colors = createColors( - appState.global.isDarkTheme, - appState.global.primaryColor, - appState.global.backgroundColor, - appState.global.onBackgroundColor - ) + appState.colors = createColors(appState.global) updateFlatLaf( - appState.global.isDarkTheme, - appState.global.backgroundColor.toAwt(), - appState.global.onBackgroundColor.toAwt() + darkTheme = appState.global.isDarkTheme, + isFollowSystemTheme = appState.global.isFollowSystemTheme, + background = appState.global.backgroundColor.toAwt(), + onBackground = appState.global.onBackgroundColor.toAwt() ) appState.saveGlobalState() selectOnBackgroundColor = false diff --git a/src/main/kotlin/ui/flatlaf/FlatLaf.kt b/src/main/kotlin/ui/flatlaf/FlatLaf.kt index 1331645..18aaa00 100644 --- a/src/main/kotlin/ui/flatlaf/FlatLaf.kt +++ b/src/main/kotlin/ui/flatlaf/FlatLaf.kt @@ -3,6 +3,7 @@ package ui.flatlaf import com.formdev.flatlaf.FlatDarkLaf import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLightLaf +import theme.isSystemDarkMode import java.awt.Color import java.awt.Font import java.awt.Insets @@ -16,8 +17,16 @@ import javax.swing.border.LineBorder /** * 初始化 FlatLaf 和文件选择器 */ -fun initializeFileChooser(darkTheme: Boolean): FutureTask { - initializeFlatLaf(darkTheme) +fun initializeFileChooser( + darkTheme: Boolean, + isFollowSystemTheme: Boolean, +): FutureTask { + if (isFollowSystemTheme) { + val isDark = isSystemDarkMode() + initializeFlatLaf(isDark) + } else{ + initializeFlatLaf(darkTheme) + } return setupFileChooser() } @@ -56,8 +65,13 @@ fun updateFlatLaf( darkTheme: Boolean, background: Color, onBackground: Color, + isFollowSystemTheme: Boolean=true, ) { - if (darkTheme) { + val isDark = if(isFollowSystemTheme){ + isSystemDarkMode() + }else darkTheme + + if (isDark) { FlatDarkLaf.setup() // Panel