mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
add support follow system light,dark mode
This commit is contained in:
parent
8f302c1259
commit
e8f552934d
@ -4,6 +4,8 @@
|
||||
|
||||
### Added
|
||||
|
||||
- support follow system Dark/Light mode
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
@ -6,6 +6,7 @@ plugins {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
|
||||
fun getOptIns() = setOf(
|
||||
|
@ -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"))
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user