add support follow system light,dark mode

This commit is contained in:
AmirHossein Abdolmotallebi 2024-08-09 23:08:03 +03:30
parent 8f302c1259
commit e8f552934d
14 changed files with 253 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.home.HomeWindow import com.abdownloadmanager.desktop.pages.home.HomeWindow
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import com.abdownloadmanager.utils.compose.ProvideDebugInfo import com.abdownloadmanager.utils.compose.ProvideDebugInfo
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -41,14 +42,17 @@ object Ui : KoinComponent {
globalAppExceptionHandler: GlobalAppExceptionHandler, globalAppExceptionHandler: GlobalAppExceptionHandler,
) { ) {
val appComponent: AppComponent = get() val appComponent: AppComponent = get()
val themeManager: ThemeManager = get()
themeManager.boot()
if (!appArguments.startSilent) { if (!appArguments.startSilent) {
appComponent.openHome() appComponent.openHome()
} }
application { application {
val theme by themeManager.currentThemeColor.collectAsState()
ProvideDebugInfo(AppInfo.isInDebugMode()) { ProvideDebugInfo(AppInfo.isInDebugMode()) {
ProvideNotificationManager { ProvideNotificationManager {
ABDownloaderTheme( ABDownloaderTheme(
theme = appComponent.theme.collectAsState().value, myColors = theme,
// uiScale = appComponent.uiScale.collectAsState().value // uiScale = appComponent.uiScale.collectAsState().value
) { ) {
ProvideGlobalExceptionHandler(globalAppExceptionHandler) { 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.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
/* /*
fun MyColors.asMaterial2Colors(): Colors { fun MyColors.asMaterial2Colors(): Colors {
@ -67,6 +68,7 @@ val darkColors = MyColors(
onInfo = Color.White, onInfo = Color.White,
isLight = false, isLight = false,
name = "Dark", name = "Dark",
id = "dark",
) )
val lightColors = MyColors( val lightColors = MyColors(
primary = Color(0xFF4791BF), primary = Color(0xFF4791BF),
@ -91,6 +93,7 @@ val lightColors = MyColors(
onInfo = Color.White, onInfo = Color.White,
isLight = true, isLight = true,
name = "Light", name = "Light",
id = "light",
) )
private val textSizes = TextSizes( private val textSizes = TextSizes(
@ -103,16 +106,10 @@ private val textSizes = TextSizes(
@Composable @Composable
fun ABDownloaderTheme( fun ABDownloaderTheme(
theme: String, myColors: MyColors,
// uiScale: Float? = null, // uiScale: Float? = null,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val myColors = if (theme == "light") {
lightColors
} else {
darkColors
}
CompositionLocalProvider( CompositionLocalProvider(
LocalMyColors provides AnimatedColors(myColors, tween(500)), LocalMyColors provides AnimatedColors(myColors, tween(500)),
// LocalUiScale provides uiScale, // LocalUiScale provides uiScale,

View File

@ -20,6 +20,7 @@ val myColors
@Stable @Stable
class MyColors( class MyColors(
val id:String,
val name:String, val name:String,
@ -163,5 +164,6 @@ fun AnimatedColors(
onInfo=onInfo, onInfo=onInfo,
isLight = isLight, isLight = isLight,
name = toBeAnimated.name, 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.runtime.collectAsState
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import java.awt.Window import java.awt.Window
@ -67,7 +68,7 @@ private class GlobalExceptionHandlerImpl : GlobalAppExceptionHandler {
} }
} }
ABDownloaderTheme( ABDownloaderTheme(
"dark", ThemeManager.DefaultTheme,
) { ) {
ErrorWindow(throwable, close) 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" } arrow-opticKsp = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = "arrow" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
osThemeDetector = "com.github.Dansoftowner:jSystemThemeDetector:3.9.1"
[plugins] [plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }