Merge pull request #18 from amir1376/15-feature-system-auto-theme

Support follow system light,dark mode
This commit is contained in:
AmirHossein Abdolmotallebi 2024-08-09 23:11:29 +03:30 committed by GitHub
commit c78e2306e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 253 additions and 38 deletions

View File

@ -4,6 +4,8 @@
### Added
- support follow system Dark/Light mode
### Changed
### Deprecated

View File

@ -6,6 +6,7 @@ plugins {
repositories {
mavenCentral()
google()
maven("https://jitpack.io")
}
fun getOptIns() = setOf(

View File

@ -13,7 +13,9 @@ plugins {
id(Plugins.aboutLibraries)
// id(MyPlugins.proguardDesktop)
}
repositories{
maven("https://jitpack.io")
}
dependencies {
implementation(libs.decompose)
implementation(libs.decompose.jbCompose)
@ -41,9 +43,13 @@ dependencies {
implementation(libs.arrow.core)
implementation(libs.arrow.optics)
ksp(libs.arrow.opticKsp)
implementation(libs.androidx.datastore)
implementation(libs.aboutLibraries.core)
implementation(libs.osThemeDetector)
implementation(project(":downloader:core"))
implementation(project(":downloader:monitor"))

View File

@ -4,6 +4,7 @@ import com.abdownloadmanager.desktop.AppArguments
import com.abdownloadmanager.integration.IntegrationHandler
import com.abdownloadmanager.desktop.AppComponent
import com.abdownloadmanager.desktop.integration.IntegrationHandlerImp
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import ir.amirab.downloader.queue.QueueManager
import com.abdownloadmanager.desktop.repository.AppRepository
import com.abdownloadmanager.desktop.storage.*
@ -157,6 +158,9 @@ val appModule = module {
single {
AppRepository()
}
single {
ThemeManager(get(),get())
}
single {
AppSettingsStorage(
createMyConfigPreferences(

View File

@ -6,16 +6,14 @@ import com.abdownloadmanager.desktop.repository.AppRepository
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
import com.abdownloadmanager.desktop.ui.icon.IconSource
import com.abdownloadmanager.desktop.ui.icon.MyIcons
import com.abdownloadmanager.desktop.ui.theme.darkColors
import com.abdownloadmanager.desktop.ui.theme.lightColors
import com.abdownloadmanager.desktop.utils.BaseComponent
import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
import ir.amirab.util.flow.mapTwoWayStateFlow
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
import androidx.compose.runtime.*
import com.arkivanov.decompose.ComponentContext
import ir.amirab.util.FileUtils
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
import kotlinx.coroutines.CoroutineScope
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -25,7 +23,8 @@ sealed class SettingSections(
val name: String,
) {
data object Appearance : SettingSections(MyIcons.appearance, "Appearance")
// TODO ADD Network section (proxy , etc..)
// TODO ADD Network section (proxy , etc..)
// data object Network : SettingSections(MyIcons.network, "Network")
data object DownloadEngine : SettingSections(MyIcons.downloadEngine, "Download Engine")
data object BrowserIntegration : SettingSections(MyIcons.network, "Browser Integration")
@ -47,6 +46,7 @@ fun threadCountConfig(appRepository: AppRepository): IntConfigurable {
},
)
}
fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable {
return BooleanConfigurable(
title = "Dynamic part creation",
@ -55,7 +55,7 @@ fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable
describe = {
if (it) {
"Enabled"
}else{
} else {
"Disabled"
}
},
@ -131,23 +131,26 @@ fun uiScaleConfig(appSettings: AppSettings): EnumConfigurable<Float?> {
}
*/
fun themeConfig(appSettings: AppSettingsStorage, scope: CoroutineScope): ThemeConfigurable {
val defaultTheme = "dark"
val themes = mapOf(
"dark" to darkColors,
"light" to lightColors
)
fun themeConfig(
themeManager: ThemeManager,
scope: CoroutineScope,
): ThemeConfigurable {
val currentThemeName = themeManager.currentThemeInfo
val themes = themeManager.possibleThemesToSelect
return ThemeConfigurable(
title = "Theme",
description = "Select theme",
//maybe try a better way?
backedBy = appSettings.theme.mapTwoWayStateFlow({
themes[it]?:themes[defaultTheme]!!
}, { myColors ->
themes.entries.firstOrNull() { it.value == myColors }?.key ?: defaultTheme
}),
possibleValues = listOf(darkColors, lightColors),
describe = { it.name },
backedBy = createMutableStateFlowFromStateFlow(
flow = currentThemeName,
updater = {
themeManager.setTheme(it.id)
},
scope = scope,
),
possibleValues = themes.value,
describe = {
it.name
},
)
}
@ -166,6 +169,7 @@ fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
}
)
}
fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable {
return BooleanConfigurable(
title = "Notification Sound",
@ -221,11 +225,12 @@ class SettingsComponent(
ContainsEffects<SettingPageEffects> by supportEffects() {
val appSettings by inject<AppSettingsStorage>()
val appRepository by inject<AppRepository>()
val themeManager by inject<ThemeManager>()
val allConfigs = object : SettingSectionGetter {
override operator fun get(key: SettingSections): List<Configurable<*>> {
return when (key) {
Appearance -> listOf(
themeConfig(appSettings, scope),
themeConfig(themeManager, scope),
// uiScaleConfig(appSettings),
autoStartConfig(appSettings),
playSoundNotification(appSettings),

View File

@ -0,0 +1,160 @@
package com.abdownloadmanager.desktop.pages.settings
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
import com.abdownloadmanager.desktop.ui.theme.MyColors
import com.abdownloadmanager.desktop.ui.theme.SystemThemeDetector
import com.abdownloadmanager.desktop.ui.theme.darkColors
import com.abdownloadmanager.desktop.ui.theme.lightColors
import ir.amirab.util.flow.combineStateFlows
import ir.amirab.util.flow.mapStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
class ThemeManager(
private val scope: CoroutineScope,
private val appSettings: AppSettingsStorage,
) {
companion object {
val defaultThemes = listOf(
darkColors,
lightColors,
)
val defaultDarkTheme = darkColors
val defaultLightTheme = lightColors
val DefaultTheme = defaultDarkTheme
val DEFAULT_THEME_ID = DefaultTheme.id
val systemThemeInfo = ThemeInfo(
id = "system",
name = "System",
color = Color.Gray,
)
}
private val _availableThemes = MutableStateFlow(emptyList<MyColors>())
val availableThemes = _availableThemes.asStateFlow()
private fun getThemeById(themeId: String): MyColors? {
return availableThemes.value.find {
it.id == themeId
}
}
val possibleThemesToSelect = availableThemes.mapStateFlow {
buildList {
addAll(it.map {
it.toThemeInfo()
})
if (osThemeDetector.isSupported) {
add(systemThemeInfo)
}
}
}
private val themeIds = possibleThemesToSelect.mapStateFlow {
it.map { it.id }
}
val currentThemeInfo = combineStateFlows(
appSettings.theme, possibleThemesToSelect
) { themeId, possibleThemes ->
possibleThemes.find {
it.id == themeId
} ?: possibleThemes.find {
it.id == DEFAULT_THEME_ID
}!!
}
private val osThemeDetector = SystemThemeDetector()
private var osDarkModeFlow = MutableStateFlow(true)
val currentThemeColor = combineStateFlows(
themeIds, appSettings.theme, osDarkModeFlow
) { themes, themeId, osThemeIsDark ->
if (themeId == systemThemeInfo.id) {
if (osThemeIsDark) {
defaultDarkTheme
} else {
defaultLightTheme
}
} else {
if (themes.contains(themeId)) {
getThemeById(themeId)!!
} else {
defaultDarkTheme
}
}
}
fun setTheme(themeId: String) {
synchronized(this) {
if (themeId == systemThemeInfo.id) {
registerSystemThemeDetector()
} else {
unRegisterSystemThemeDetector()
}
if (themeIds.value.contains(themeId)) {
appSettings.theme.value = themeId
} else {
// theme id in setting is invalid update it
appSettings.theme.value = DEFAULT_THEME_ID
}
}
}
@Volatile
private var booted = false
fun boot() {
if (booted) return
// now we can load custom themes here
// loadCustomThemes()
//
_availableThemes.update {
it.plus(defaultThemes)
}
setTheme(appSettings.theme.value)
booted = true
}
private var osUpdateFlowJob: Job? = null
private fun registerSystemThemeDetector() {
osUpdateFlowJob?.cancel()
if (osThemeDetector.isSupported) {
// update immediately
osDarkModeFlow.value = osThemeDetector.isDark
osUpdateFlowJob = osThemeDetector.systemThemeFlow.onEach { isDark ->
osDarkModeFlow.value = isDark
}.launchIn(scope)
}
}
private fun unRegisterSystemThemeDetector() {
osUpdateFlowJob?.cancel()
osUpdateFlowJob = null
}
}
/**
* This is for demonstration purposes of a theme
*/
@Stable
data class ThemeInfo(
val id: String,
val name: String,
val color: Color,
)
private fun MyColors.toThemeInfo(): ThemeInfo {
return ThemeInfo(
id = id,
name = name,
color = surface,
)
}

View File

@ -1,5 +1,8 @@
package com.abdownloadmanager.desktop.pages.settings.configurable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import com.abdownloadmanager.desktop.pages.settings.ThemeInfo
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable.RenderMode
import com.abdownloadmanager.desktop.ui.theme.MyColors
import kotlinx.coroutines.flow.MutableStateFlow
@ -230,12 +233,12 @@ open class EnumConfigurable<T>(
class ThemeConfigurable(
title: String,
description: String,
backedBy: MutableStateFlow<MyColors>,
describe: (MyColors) -> String,
possibleValues: List<MyColors>,
backedBy: MutableStateFlow<ThemeInfo>,
describe: (ThemeInfo) -> String,
possibleValues: List<ThemeInfo>,
enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue,
) : BaseEnumConfigurable<MyColors>(
) : BaseEnumConfigurable<ThemeInfo>(
title = title,
description = description,
backedBy = backedBy,

View File

@ -1,10 +1,8 @@
package com.abdownloadmanager.desktop.pages.settings.configurable.widgets
import com.abdownloadmanager.desktop.pages.settings.configurable.EnumConfigurable
import com.abdownloadmanager.desktop.pages.settings.configurable.ThemeConfigurable
import com.abdownloadmanager.desktop.ui.theme.myColors
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
import com.abdownloadmanager.desktop.utils.div
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
@ -18,7 +16,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun RenderThemeConfig(cfg: ThemeConfigurable, modifier: Modifier) {
@ -49,7 +46,7 @@ fun RenderThemeConfig(cfg: ThemeConfigurable, modifier: Modifier) {
)
.padding(1.dp)
.background(
it.surface,
it.color,
)
.size(16.dp)
)

View File

@ -26,6 +26,7 @@ import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import androidx.compose.runtime.*
import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.home.HomeWindow
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -41,14 +42,17 @@ object Ui : KoinComponent {
globalAppExceptionHandler: GlobalAppExceptionHandler,
) {
val appComponent: AppComponent = get()
val themeManager: ThemeManager = get()
themeManager.boot()
if (!appArguments.startSilent) {
appComponent.openHome()
}
application {
val theme by themeManager.currentThemeColor.collectAsState()
ProvideDebugInfo(AppInfo.isInDebugMode()) {
ProvideNotificationManager {
ABDownloaderTheme(
theme = appComponent.theme.collectAsState().value,
myColors = theme,
// uiScale = appComponent.uiScale.collectAsState().value
) {
ProvideGlobalExceptionHandler(globalAppExceptionHandler) {

View File

@ -24,6 +24,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
/*
fun MyColors.asMaterial2Colors(): Colors {
@ -67,6 +68,7 @@ val darkColors = MyColors(
onInfo = Color.White,
isLight = false,
name = "Dark",
id = "dark",
)
val lightColors = MyColors(
primary = Color(0xFF4791BF),
@ -91,6 +93,7 @@ val lightColors = MyColors(
onInfo = Color.White,
isLight = true,
name = "Light",
id = "light",
)
private val textSizes = TextSizes(
@ -103,16 +106,10 @@ private val textSizes = TextSizes(
@Composable
fun ABDownloaderTheme(
theme: String,
myColors: MyColors,
// uiScale: Float? = null,
content: @Composable () -> Unit,
) {
val myColors = if (theme == "light") {
lightColors
} else {
darkColors
}
CompositionLocalProvider(
LocalMyColors provides AnimatedColors(myColors, tween(500)),
// LocalUiScale provides uiScale,

View File

@ -20,6 +20,7 @@ val myColors
@Stable
class MyColors(
val id:String,
val name:String,
@ -163,5 +164,6 @@ fun AnimatedColors(
onInfo=onInfo,
isLight = isLight,
name = toBeAnimated.name,
id = toBeAnimated.id,
)
}

View File

@ -0,0 +1,32 @@
package com.abdownloadmanager.desktop.ui.theme
import com.jthemedetecor.OsThemeDetector
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
class SystemThemeDetector {
val isSupported by lazy {
OsThemeDetector.isSupported()
}
private val detector by lazy { OsThemeDetector.getDetector() }
private val isSystemDarkFlowByLibrary = callbackFlow<Boolean> {
val listener: (Boolean) -> Unit = { isDark: Boolean ->
trySend(isDark)
}
detector.registerListener(listener)
awaitClose {
detector.removeListener(listener)
}
}
val isDark = detector.isDark
val systemThemeFlow = flow {
if (!isSupported){
return@flow
}
emit(detector.isDark)
emitAll(isSystemDarkFlowByLibrary)
}
}

View File

@ -8,6 +8,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.awt.Window
@ -67,7 +68,7 @@ private class GlobalExceptionHandlerImpl : GlobalAppExceptionHandler {
}
}
ABDownloaderTheme(
"dark",
ThemeManager.DefaultTheme,
) {
ErrorWindow(throwable, close)
}

View File

@ -105,6 +105,7 @@ arrow-optics = { module = "io.arrow-kt:arrow-optics", version.ref = "arrow" }
arrow-opticKsp = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = "arrow" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
osThemeDetector = "com.github.Dansoftowner:jSystemThemeDetector:3.9.1"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }