diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index c480d8fd86..228d5b3013 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -682,7 +682,8 @@ data class User( override val showNtfs: Boolean, val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, - val viewPwdHash: UserPwdHash? + val viewPwdHash: UserPwdHash?, + val uiThemes: ThemeModeOverrides? = null, ): NamedChat, UserLike { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName @@ -709,6 +710,7 @@ data class User( sendRcptsContacts = true, sendRcptsSmallGroups = false, viewPwdHash = null, + uiThemes = null, ) } } @@ -1042,7 +1044,7 @@ data class Contact( val chatTs: Instant?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, - val uiThemes: Map? = null, + val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" @@ -1248,7 +1250,7 @@ data class GroupInfo ( override val createdAt: Instant, override val updatedAt: Instant, val chatTs: Instant?, - val uiThemes: Map? = null, + val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index ed1a60c8a0..017d9ead87 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1156,7 +1156,7 @@ object ChatController { return false } - suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: Map?): Boolean { + suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?): Boolean { val r = sendCmd(rh, CC.ApiSetChatUIThemes(chatId, themes)) if (r is CR.CmdOk) return true Log.e(TAG, "apiSetChatUIThemes bad response: ${r.responseType} ${r.details}") @@ -2464,7 +2464,7 @@ sealed class CC { class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() class ApiSetUserUIThemes(val userId: Long, val themes: Map?): CC() - class ApiSetChatUIThemes(val chatId: String, val themes: Map?): CC() + class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC() class ApiCreateMyAddress(val userId: Long): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() @@ -2609,8 +2609,8 @@ sealed class CC { is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" - is ApiSetUserUIThemes -> "/_set theme user $userId ${json.encodeToString(themes)}" - is ApiSetChatUIThemes -> "/_set theme $chatId ${json.encodeToString(themes)}" + is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}" + is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}" is ApiCreateMyAddress -> "/_address $userId" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" @@ -5433,7 +5433,9 @@ data class AppSettings( var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null, var iosCallKitEnabled: Boolean? = null, var iosCallKitCallsInRecents: Boolean? = null, + var uiProfileImageCornerRadius: Float? = null, var uiColorScheme: String? = null, + var uiDarkColorScheme: String? = null, var uiThemes: Map? = null, ) { fun prepareForExport(): AppSettings { @@ -5458,7 +5460,9 @@ data class AppSettings( if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen } if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled } if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme } + if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme } if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes } return empty } @@ -5491,7 +5495,9 @@ data class AppSettings( androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) } iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) } iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) } + uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) } uiColorScheme?.let { def.currentTheme.set(it) } + uiDarkColorScheme?.let { def.systemDarkTheme.set(it) } uiThemes?.let { def.themeOverrides.set(it) } } @@ -5517,7 +5523,9 @@ data class AppSettings( androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW, iosCallKitEnabled = true, iosCallKitCallsInRecents = false, + uiProfileImageCornerRadius = 22.5f, uiColorScheme = DefaultTheme.SYSTEM.themeName, + uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName, uiThemes = null, ) @@ -5544,7 +5552,9 @@ data class AppSettings( androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()), iosCallKitEnabled = def.iosCallKitEnabled.get(), iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(), + uiProfileImageCornerRadius = def.profileImageCornerRadius.get(), uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM.themeName, + uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName, uiThemes = def.themeOverrides.get(), ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt index bb0a090b94..168de989f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -42,12 +42,12 @@ fun runMigrations() { ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) } lastMigration.set(117) - } else if (lastMigration.get() < 201) { + } else if (lastMigration.get() < 203) { // Making theme keys lowercase as API expects ChatController.appPrefs.themeOverrides.set(ChatController.appPrefs.themeOverrides.get().map { it.key.lowercase() to it.value }.toMap()) ChatController.appPrefs.currentTheme.set(ChatController.appPrefs.currentTheme.get()?.lowercase()) ChatController.appPrefs.systemDarkTheme.set(ChatController.appPrefs.systemDarkTheme.get()?.lowercase()) - lastMigration.set(201) + lastMigration.set(203) } else { lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE) break diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 85ae6b8176..1de321c664 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -23,6 +23,8 @@ enum class DefaultTheme { val themeName: String get() = name.lowercase() + val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK + // Call it only with base theme, not SYSTEM fun hasChangedAnyColor(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Boolean { val palette = when (this) { @@ -49,6 +51,12 @@ enum class DefaultTheme { } } +@Serializable +enum class DefaultThemeMode { + @SerialName("light") LIGHT, + @SerialName("dark") DARK +} + @Stable class AppColors( title: Color, @@ -166,61 +174,12 @@ data class ThemeColors( val title: String? = null, val sentMessage: String? = null, val receivedMessage: String? = null, -) { - fun toColors(base: DefaultTheme, backgroundTheme: ThemeColors?): Colors { - val baseColors = when (base) { - DefaultTheme.LIGHT -> LightColorPalette - DefaultTheme.DARK -> DarkColorPalette - DefaultTheme.SIMPLEX -> SimplexColorPalette - // shouldn't be here - DefaultTheme.SYSTEM -> LightColorPalette - } - return baseColors.copy( - primary = primary?.colorFromReadableHex() ?: backgroundTheme?.primary?.colorFromReadableHex() ?: baseColors.primary, - primaryVariant = primaryVariant?.colorFromReadableHex() ?: backgroundTheme?.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, - secondary = secondary?.colorFromReadableHex() ?: backgroundTheme?.secondary?.colorFromReadableHex() ?: baseColors.secondary, - secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: backgroundTheme?.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, - background = background?.colorFromReadableHex() ?: backgroundTheme?.background?.colorFromReadableHex() ?: baseColors.background, - surface = surface?.colorFromReadableHex() ?: backgroundTheme?.surface?.colorFromReadableHex() ?: baseColors.surface, - ) - } - - fun toAppColors(base: DefaultTheme, backgroundTheme: ThemeColors?): AppColors { - val baseColors = when (base) { - DefaultTheme.LIGHT -> LightColorPaletteApp - DefaultTheme.DARK -> DarkColorPaletteApp - DefaultTheme.SIMPLEX -> SimplexColorPaletteApp - // shouldn't be here - DefaultTheme.SYSTEM -> LightColorPaletteApp - } - return baseColors.copy( - title = title?.colorFromReadableHex() ?: backgroundTheme?.title?.colorFromReadableHex() ?: baseColors.title, - sentMessage = sentMessage?.colorFromReadableHex() ?: backgroundTheme?.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, - receivedMessage = receivedMessage?.colorFromReadableHex() ?: backgroundTheme?.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, - ) - } - - fun withFilledColors(base: DefaultTheme, backgroundTheme: ThemeColors?): ThemeColors { - val c = toColors(base, backgroundTheme) - val ac = toAppColors(base, backgroundTheme) - return ThemeColors( - primary = c.primary.toReadableHex(), - primaryVariant = c.primaryVariant.toReadableHex(), - secondary = c.secondary.toReadableHex(), - secondaryVariant = c.secondaryVariant.toReadableHex(), - background = c.background.toReadableHex(), - surface = c.surface.toReadableHex(), - title = ac.title.toReadableHex(), - sentMessage = ac.sentMessage.toReadableHex(), - receivedMessage = ac.receivedMessage.toReadableHex(), - ) - } -} +) @Serializable data class ThemeWallpaper ( val preset: String? = null, - val scale: Float = 1f, + val scale: Float? = null, val scaleType: BackgroundImageScaleType? = null, val background: String? = null, val tint: String? = null, @@ -232,11 +191,11 @@ data class ThemeWallpaper ( background = background?.colorFromReadableHex(), tint = tint?.colorFromReadableHex(), type = if (preset != null) { - BackgroundImageType.Repeated(filename = preset, scale) + BackgroundImageType.Repeated(filename = preset, scale ?: 1f) } else if (imageFile != null) { BackgroundImageType.Static( filename = imageFile, - scale, + scale ?: 1f, scaleType ?: BackgroundImageScaleType.FILL ) } else @@ -244,7 +203,8 @@ data class ThemeWallpaper ( ) } - fun withFilledWallpaperBase64(): ThemeWallpaper { + // LALAL + fun withFilledWallpaperBase64(perUserTheme: ThemeModeOverride?): ThemeWallpaper { val aw = toAppWallpaper() val type = aw.type return ThemeWallpaper( @@ -306,6 +266,98 @@ data class ThemeOverrides ( val wallpaper: ThemeWallpaper = ThemeWallpaper(), ) { fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides { + return copy( + colors = when (name) { + ThemeColor.PRIMARY -> colors.copy(primary = color) + ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) + ThemeColor.SECONDARY -> colors.copy(secondary = color) + ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color) + ThemeColor.BACKGROUND -> colors.copy(background = color) + ThemeColor.SURFACE -> colors.copy(surface = color) + ThemeColor.TITLE -> colors.copy(title = color) + ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) + ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) + ThemeColor.WALLPAPER_BACKGROUND -> colors.copy() + ThemeColor.WALLPAPER_TINT -> colors.copy() + }, wallpaper = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaper.copy(background = color) + ThemeColor.WALLPAPER_TINT -> wallpaper.copy(tint = color) + else -> wallpaper.copy() + } + ) + } + + fun toColors(base: DefaultTheme, perChatTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?, perUserTheme: ThemeColors?): Colors { + val baseColors = when (base) { + DefaultTheme.LIGHT -> LightColorPalette + DefaultTheme.DARK -> DarkColorPalette + DefaultTheme.SIMPLEX -> SimplexColorPalette + // shouldn't be here + DefaultTheme.SYSTEM -> LightColorPalette + } + return baseColors.copy( + primary = perChatTheme?.primary?.colorFromReadableHex() ?: presetWallpaperTheme?.primary?.colorFromReadableHex() ?: perUserTheme?.primary?.colorFromReadableHex() ?: colors.primary?.colorFromReadableHex() ?: baseColors.primary, + primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?: perUserTheme?.primaryVariant?.colorFromReadableHex() ?: colors.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, + secondary = perChatTheme?.secondary?.colorFromReadableHex() ?: presetWallpaperTheme?.secondary?.colorFromReadableHex() ?: perUserTheme?.secondary?.colorFromReadableHex() ?: colors.secondary?.colorFromReadableHex() ?: baseColors.secondary, + secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?: perUserTheme?.secondaryVariant?.colorFromReadableHex() ?: colors.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, + background = perChatTheme?.background?.colorFromReadableHex() ?: presetWallpaperTheme?.background?.colorFromReadableHex() ?: perUserTheme?.background?.colorFromReadableHex() ?: colors.background?.colorFromReadableHex() ?: baseColors.background, + surface = perChatTheme?.surface?.colorFromReadableHex() ?: presetWallpaperTheme?.surface?.colorFromReadableHex() ?: perUserTheme?.surface?.colorFromReadableHex() ?: colors.surface?.colorFromReadableHex() ?: baseColors.surface, + ) + } + + fun toAppColors(base: DefaultTheme, perChatTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?, perUserTheme: ThemeColors?): AppColors { + val baseColors = when (base) { + DefaultTheme.LIGHT -> LightColorPaletteApp + DefaultTheme.DARK -> DarkColorPaletteApp + DefaultTheme.SIMPLEX -> SimplexColorPaletteApp + // shouldn't be here + DefaultTheme.SYSTEM -> LightColorPaletteApp + } + return baseColors.copy( + title = perChatTheme?.title?.colorFromReadableHex() ?: presetWallpaperTheme?.title?.colorFromReadableHex() ?: perUserTheme?.title?.colorFromReadableHex() ?: colors.title?.colorFromReadableHex() ?: baseColors.title, + sentMessage = perChatTheme?.sentMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?: perUserTheme?.sentMessage?.colorFromReadableHex() ?: colors.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, + receivedMessage = perChatTheme?.receivedMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?: perUserTheme?.receivedMessage?.colorFromReadableHex() ?: colors.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, + ) + } + + fun withFilledColors(base: DefaultTheme, perChatTheme: ThemeColors?, perUserTheme: ThemeColors?, appSettingsTheme: ThemeColors?): ThemeColors { + val c = toColors(base, perChatTheme, perUserTheme, appSettingsTheme) + val ac = toAppColors(base, perChatTheme, perUserTheme, appSettingsTheme) + return ThemeColors( + primary = c.primary.toReadableHex(), + primaryVariant = c.primaryVariant.toReadableHex(), + secondary = c.secondary.toReadableHex(), + secondaryVariant = c.secondaryVariant.toReadableHex(), + background = c.background.toReadableHex(), + surface = c.surface.toReadableHex(), + title = ac.title.toReadableHex(), + sentMessage = ac.sentMessage.toReadableHex(), + receivedMessage = ac.receivedMessage.toReadableHex(), + ) + } +} + +@Serializable +data class ThemeModeOverrides ( + val light: ThemeModeOverride? = null, + val dark: ThemeModeOverride? = null +) { + fun preferredTheme(baseTheme: DefaultTheme = CurrentColors.value.base): ThemeModeOverride? { + return when (baseTheme) { + DefaultTheme.LIGHT -> light + else -> dark + } + } +} + +@Serializable +data class ThemeModeOverride ( + val mode: DefaultThemeMode = CurrentColors.value.base.mode, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) { + fun withUpdatedColor(name: ThemeColor, color: String?): ThemeModeOverride { + val wallpaper = wallpaper ?: ThemeWallpaper() return copy(colors = when (name) { ThemeColor.PRIMARY -> colors.copy(primary = color) ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) @@ -325,20 +377,13 @@ data class ThemeOverrides ( } ) } -} - -fun Map.preferredTheme(baseTheme: DefaultTheme = CurrentColors.value.base): ThemeOverrides? { - val idealTheme = get(baseTheme.themeName) - if (idealTheme != null) return idealTheme - - val light = get(DefaultTheme.LIGHT.themeName) - val dark = get(DefaultTheme.DARK.themeName) - val simplex = get(DefaultTheme.SIMPLEX.themeName) - return when (baseTheme.themeName) { - DefaultTheme.LIGHT.themeName -> dark ?: simplex - DefaultTheme.DARK.themeName -> simplex ?: light - DefaultTheme.SIMPLEX.themeName -> dark ?: light - else -> null + companion object { + fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride = + ThemeModeOverride( + mode = mode, + colors = ThemeOverrides().withFilledColors(base, null, null, null), + wallpaper = ThemeWallpaper() + ) } } @@ -427,7 +472,7 @@ val SimplexColorPaletteApp = AppColors( receivedMessage = Color(0x20B1B0B5), ) -val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) +val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(isInNightMode(), null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())) @Composable fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight @@ -466,7 +511,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { LaunchedEffect(darkTheme) { // For preview if (darkTheme != null) - CurrentColors.value = ThemeManager.currentColors(darkTheme) + CurrentColors.value = ThemeManager.currentColors(darkTheme, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) } val systemDark = isSystemInDarkTheme() LaunchedEffect(systemDark) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 66792ab5ad..f1b0c2c11f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,10 +1,12 @@ package chat.simplex.common.ui.theme import androidx.compose.material.Colors +import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.font.FontFamily import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel import chat.simplex.res.MR import chat.simplex.common.platform.platform import chat.simplex.common.views.helpers.* @@ -17,7 +19,7 @@ expect val EmojiFont: FontFamily object ThemeManager { private val appPrefs: AppPreferences = ChatController.appPrefs - data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper) + data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper = AppWallpaper()) private fun systemDarkThemeColors(): Pair = when (appPrefs.systemDarkTheme.get()) { DefaultTheme.DARK.themeName -> DarkColorPalette to DefaultTheme.DARK @@ -25,39 +27,40 @@ object ThemeManager { else -> SimplexColorPalette to DefaultTheme.SIMPLEX } - fun currentColors(darkForSystemTheme: Boolean, themeOverrides: Map = appPrefs.themeOverrides.get()): ActiveTheme { + fun currentColors(darkForSystemTheme: Boolean, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: Map): ActiveTheme { val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.themeName) { themeName } else { if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName } - val theme = themeOverrides[nonSystemThemeName] + val theme = appSettingsTheme[nonSystemThemeName] + val modeOverrides = if (darkForSystemTheme) perUserTheme?.dark else perUserTheme?.light val baseTheme = when (nonSystemThemeName) { - DefaultTheme.LIGHT.themeName -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.themeName -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.themeName -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) - else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + DefaultTheme.LIGHT.themeName -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + DefaultTheme.DARK.themeName -> ActiveTheme(DefaultTheme.DARK.themeName, DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) + DefaultTheme.SIMPLEX.themeName -> ActiveTheme(DefaultTheme.SIMPLEX.themeName, DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) + else -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) } - if (theme == null) { - return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third, AppWallpaper()) + if (theme == null && perUserTheme == null) { + return ActiveTheme(themeName, baseTheme.base, baseTheme.colors, baseTheme.appColors, baseTheme.wallpaper) } - val backgroundTheme = if (theme.wallpaper.preset != null) PredefinedBackgroundImage.from(theme.wallpaper.preset)?.colors?.get(theme.base) else null - return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base, backgroundTheme), theme.colors.toAppColors(theme.base, backgroundTheme), theme.wallpaper.toAppWallpaper()) + val wallpaperTheme = when { + perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PredefinedBackgroundImage.from(perChatTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + modeOverrides?.wallpaper != null -> if (modeOverrides.wallpaper.preset != null) PredefinedBackgroundImage.from(modeOverrides.wallpaper.preset)?.colors?.get(baseTheme.base) else null + else -> if (theme?.wallpaper?.preset != null) PredefinedBackgroundImage.from(theme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + } + val themeOrEmpty = theme ?: ThemeOverrides() + return ActiveTheme( + themeName, + baseTheme.base, + themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, wallpaperTheme, modeOverrides?.colors), + themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, wallpaperTheme, modeOverrides?.colors), + perChatTheme?.wallpaper?.toAppWallpaper() ?: modeOverrides?.wallpaper?.toAppWallpaper() ?: themeOrEmpty.wallpaper.toAppWallpaper() + ) } - fun overriddenColors(theme: ThemeOverrides): ActiveTheme { - val baseTheme = when (theme.base.themeName) { - DefaultTheme.LIGHT.themeName -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.themeName -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.themeName -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) - else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) - } - val backgroundTheme = if (theme.wallpaper.preset != null) PredefinedBackgroundImage.from(theme.wallpaper.preset)?.colors?.get(theme.base) else null - return ActiveTheme(theme.base.themeName, baseTheme.first, theme.colors.toColors(theme.base, backgroundTheme), theme.colors.toAppColors(theme.base, backgroundTheme), theme.wallpaper.toAppWallpaper()) - } - - fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { + fun currentThemeOverridesForExport(darkForSystemTheme: Boolean, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?): ThemeOverrides { val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.themeName) { themeName @@ -66,8 +69,16 @@ object ThemeManager { } val overrides = appPrefs.themeOverrides.get().toMutableMap() val nonFilledTheme = overrides.getOrDefault(nonSystemThemeName, ThemeOverrides()) - val backgroundTheme = if (nonFilledTheme.wallpaper.preset != null) PredefinedBackgroundImage.from(nonFilledTheme.wallpaper.preset)?.colors?.get(nonFilledTheme.base) else null - return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base, backgroundTheme), wallpaper = nonFilledTheme.wallpaper.withFilledWallpaperBase64()) + val modeOverrides = if (darkForSystemTheme) perUserTheme?.dark else perUserTheme?.light + val wallpaperTheme = when { + perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PredefinedBackgroundImage.from(perChatTheme.wallpaper.preset)?.colors?.get(nonFilledTheme.base) else null + modeOverrides?.wallpaper != null -> if (modeOverrides.wallpaper.preset != null) PredefinedBackgroundImage.from(modeOverrides.wallpaper.preset)?.colors?.get(nonFilledTheme.base) else null + else -> if (nonFilledTheme.wallpaper.preset != null) PredefinedBackgroundImage.from(nonFilledTheme.wallpaper.preset)?.colors?.get(nonFilledTheme.base) else null + } + return nonFilledTheme.copy( + colors = nonFilledTheme.withFilledColors(CurrentColors.value.base, perChatTheme?.colors, modeOverrides?.colors, wallpaperTheme), + wallpaper = nonFilledTheme.wallpaper.withFilledWallpaperBase64(modeOverrides) + ) } // colors, default theme enum, localized name of theme @@ -106,13 +117,13 @@ object ThemeManager { fun applyTheme(theme: String, darkForSystemTheme: Boolean) { appPrefs.currentTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides.get()) + CurrentColors.value = currentColors(darkForSystemTheme, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() } fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { appPrefs.systemDarkTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides.get()) + CurrentColors.value = currentColors(darkForSystemTheme, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference> = appPrefs.themeOverrides) { @@ -132,7 +143,12 @@ object ThemeManager { val prevValue = overrides.getOrDefault(nonSystemThemeName, ThemeOverrides()) overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet?.toReadableHex()) pref.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState) { + val prevValue = pref.value + pref.value = prevValue.withUpdatedColor(name, color?.toReadableHex()) } fun saveAndApplyBackgroundImage(baseTheme: DefaultTheme, type: BackgroundImageType?, pref: SharedPreference> = appPrefs.themeOverrides) { @@ -145,7 +161,16 @@ object ThemeManager { } overrides[nonSystemThemeName] = prevValue.copy(wallpaper = if (type != null) ThemeWallpaper.from(type, prevValue.wallpaper.background, prevValue.wallpaper.tint) else ThemeWallpaper()) pref.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun applyBackgroundImage(type: BackgroundImageType?, pref: MutableState) { + var prevValue = pref.value + // Overriding the whole theme on type change + if (prevValue.wallpaper?.imageFile != type?.filename && prevValue.wallpaper?.preset != type?.filename) { + prevValue = ThemeModeOverride() + } + pref.value = prevValue.copy(wallpaper = if (type != null) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) else ThemeWallpaper()) } fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference> = appPrefs.themeOverrides) { @@ -154,7 +179,7 @@ object ThemeManager { overrides[theme.base.themeName] = prevValue.copy(colors = theme.colors, wallpaper = theme.wallpaper.importFromString()) pref.set(overrides) appPrefs.currentTheme.set(theme.base.themeName) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } fun resetAllThemeColors(darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { @@ -168,7 +193,7 @@ object ThemeManager { val prevValue = overrides[nonSystemThemeName] ?: return overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper.copy(background = null, tint = null)) pref.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } fun String.colorFromReadableHex(): Color = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 3a097df10c..6942dfd718 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -29,6 +30,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -42,6 +44,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.datetime.Clock +import java.util.* @Composable fun ChatInfoView( @@ -339,24 +342,9 @@ fun ChatInfoLayout( SynchronizeConnectionButton(syncContactConnection) } - val theme = remember { - // Using existing theme overrides as an overrides for a new theme if the same BASE theme is not found in already configured map - (chat.chatInfo as ChatInfo.Direct).contact.uiThemes?.preferredTheme()?.copy(base = CurrentColors.value.base) ?: ThemeOverrides() - } - WallpaperButton { ModalManager.end.showModal { - WallpaperEditor(theme) { type, theme -> - withBGApi { - val changedThemes = ((chat.chatInfo as ChatInfo.Direct).contact.uiThemes ?: mapOf()).toMutableMap() - changedThemes[theme.base.themeName] = theme.copy(wallpaper = theme.wallpaper.withFilledWallpaperPath()) - if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { - // Remove previous image only after saving - removeBackgroundImage(theme.wallpaper.imageFile) - chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) - } - } - } + WallpaperEditorModal(chat) } } // } else if (developerTools) { @@ -707,74 +695,199 @@ fun ShareAddressButton(onClick: () -> Unit) { } @Composable -fun ModalData.WallpaperEditor(theme: ThemeOverrides, save: (BackgroundImageType?, ThemeOverrides) -> Unit) { +fun ModalData.WallpaperEditorModal(chat: Chat) { + val initialTheme = remember(CurrentColors.value.base) { + (chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes?.preferredTheme()?.copy(mode = CurrentColors.value.base.mode) + ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes?.preferredTheme()?.copy(mode = CurrentColors.value.base.mode) + ?: ThemeModeOverride() + } + WallpaperEditor( + initialTheme, + save = { applyToMode, newTheme -> + withBGApi { + var changedThemes: ThemeModeOverrides? = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides() + val changed = newTheme?.copy(mode = initialTheme.mode, wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) + changedThemes = if (applyToMode == null) { + changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK)) + } else if (changed?.mode == DefaultThemeMode.LIGHT) { + changedThemes?.copy(light = changed) + } else { + changedThemes?.copy(dark = changed) + } + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) changedThemes else null + if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { + // Remove previous image only after saving + removeBackgroundImage(initialTheme.wallpaper?.imageFile) + if (chat.chatInfo is ChatInfo.Direct) { + chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + } else if (chat.chatInfo is ChatInfo.Group) { + chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + } + } + } + }, + setDefaultsToAll = { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else DefaultTheme.SIMPLEX + val modeLight = ThemeModeOverride.withFilledAppDefaults(DefaultThemeMode.LIGHT, lightBase) + val modeDark = ThemeModeOverride.withFilledAppDefaults(DefaultThemeMode.DARK, darkBase) + val changedThemes = ThemeModeOverrides(light = modeLight, dark = modeDark) + if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { + // Remove previous image only after saving + removeBackgroundImage(initialTheme.wallpaper?.imageFile) + if (chat.chatInfo is ChatInfo.Direct) { + chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + } else if (chat.chatInfo is ChatInfo.Group) { + chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + } + } + (chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes?.preferredTheme()?.copy(mode = CurrentColors.value.base.mode) + ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes?.preferredTheme()?.copy(mode = CurrentColors.value.base.mode) + ?: ThemeModeOverride() + } + ) +} + +@Composable +fun ModalData.WallpaperEditor(theme: ThemeModeOverride, save: (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit, setDefaultsToAll: suspend () -> ThemeModeOverride) { ColumnWithScrollBar( Modifier .fillMaxSize() ) { - val baseTheme = CurrentColors.value.base - val themeOverrides = remember { stateGetOrPut("themeOverrides") { theme } } - val backgroundImageType: State = remember { derivedStateOf { BackgroundImageType.from(themeOverrides.value.wallpaper) } } - val pref = remember { - SharedPreference>( - get = { - mapOf(baseTheme.themeName to themeOverrides.value) - }, - set = { value -> - themeOverrides.value = value[baseTheme.themeName]!! - } + val applyToMode = remember { stateGetOrPutNullable("applyToMode") { null as DefaultThemeMode? } } + var showMore by remember { stateGetOrPut("showMore") { false } } + val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } + val backgroundImageType: State = remember { derivedStateOf { BackgroundImageType.from(themeModeOverride.value.wallpaper) } } + val systemDark = isInDarkTheme() + val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) { + mutableStateOf( + ThemeManager.currentColors(systemDark, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) ) } AppBarTitle(stringResource(MR.strings.settings_section_title_wallpaper)) val backgroundImage = remember { derivedStateOf { backgroundImageType.value?.image } } - val backgroundColor = remember { derivedStateOf { themeOverrides.value.wallpaper.background?.colorFromReadableHex() } } - val tintColor = remember { derivedStateOf { themeOverrides.value.wallpaper.tint?.colorFromReadableHex() } } + val backgroundColor = remember { derivedStateOf { themeModeOverride.value.wallpaper?.background?.colorFromReadableHex() } } + val tintColor = remember { derivedStateOf { themeModeOverride.value.wallpaper?.tint?.colorFromReadableHex() } } + + fun editColor(name: ThemeColor, initialColor: Color) { + ModalManager.end.showModalCloseable { close -> + AppearanceScope.ColorEditor( + name, + initialColor, + CurrentColors.collectAsState().value.base, + backgroundImageType.value, + backgroundImage.value, + backgroundColor.value, + tintColor.value, + onColorChange = { color -> + ThemeManager.applyThemeColor(name, color, themeModeOverride) + save(applyToMode.value, themeModeOverride.value) + }, + close + ) + } + } WallpaperSetupView( backgroundImageType.value, - theme.base, + CurrentColors.collectAsState().value.base, backgroundColor.value, tintColor.value, showPresetSelection = true, editColor = { name, initialColor -> - ModalManager.end.showModalCloseable { close -> - AppearanceScope.ColorEditor( - name, - initialColor, - theme.base, - backgroundImageType.value, - backgroundImage.value, - backgroundColor.value, - tintColor.value, - onColorChange = { color -> - ThemeManager.saveAndApplyThemeColor(baseTheme, name, color, pref) - }, - close - ) - } + editColor(name, initialColor) }, onColorChange = { name, color -> - ThemeManager.saveAndApplyThemeColor(baseTheme, name, color, pref) + ThemeManager.applyThemeColor(name, color, themeModeOverride) + save(applyToMode.value, themeModeOverride.value) }, onTypeChange = { type -> - ThemeManager.saveAndApplyBackgroundImage(baseTheme, type, pref) + ThemeManager.applyBackgroundImage(type, themeModeOverride) + save(applyToMode.value, themeModeOverride.value) } ) SectionSpacer() - ApplyButton { - save(backgroundImageType.value, themeOverrides.value) + ResetToGlobalThemeButton { + themeModeOverride.value = ThemeModeOverride() + save(applyToMode.value, null) } + + SetDefaultThemeButton { + if (applyToMode.value == null) { + withBGApi { + themeModeOverride.value = setDefaultsToAll() + } + } else { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(null, themeModeOverride.value) + } + } + + SectionSpacer() + + if (!showMore) { + AdvancedSettingsButton { showMore = true } + } else { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) + )} + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName, systemDark) + } + } + ) + KeyChangeEffect(theme.mode) { + themeModeOverride.value = theme + } + + SectionSpacer() + + AppearanceScope.CustomizeThemeColorsSection(currentTheme) { name, color -> editColor(name, color) } + } + + SectionBottomSpacer() } } @Composable -private fun ApplyButton(onClick: () -> Unit) { +private fun ResetToGlobalThemeButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(MR.strings.chat_theme_reset_to_global_theme), color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun SetDefaultThemeButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(MR.strings.chat_theme_set_default_theme), color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun AdvancedSettingsButton(onClick: () -> Unit) { SettingsActionItem( - painterResource(MR.images.ic_check), - stringResource(MR.strings.background_image_apply), + painterResource(MR.images.ic_arrow_downward), + stringResource(MR.strings.background_image_advanced_settings), click = onClick ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 296d3a96cc..071ed66565 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -117,8 +117,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: val clipboard = LocalClipboardManager.current when (chat.chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { - val themeOverrides = remember(chat.chatInfo) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredTheme() else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredTheme() else null } - val overrides = if (themeOverrides != null) ThemeManager.overriddenColors(themeOverrides) else null + val perChatTheme = remember(chat.chatInfo, CurrentColors.value.base) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredTheme() else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredTheme() else null } + val overrides = if (perChatTheme != null) ThemeManager.currentColors(isInDarkTheme(), perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.state.value) else null SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { ChatLayout( chat, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 9a4b7b0323..3c65aa581c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -235,24 +235,9 @@ fun GroupChatInfoLayout( SendReceiptsOptionDisabled() } - val theme = remember { - // Using existing theme overrides as an overrides for a new theme if the same BASE theme is not found in already configured map - (chat.chatInfo as ChatInfo.Group).groupInfo.uiThemes?.preferredTheme()?.copy(base = CurrentColors.value.base) ?: ThemeOverrides() - } WallpaperButton { ModalManager.end.showModal { - WallpaperEditor(theme) { type, theme -> - withBGApi { - val changedThemes = ((chat.chatInfo as ChatInfo.Direct).contact.uiThemes ?: mapOf()).toMutableMap() - changedThemes[theme.base.themeName] = theme.copy(wallpaper = theme.wallpaper.withFilledWallpaperPath()) - if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { - // Remove previous image only after saving - removeBackgroundImage(theme.wallpaper.imageFile) - chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) - } - - } - } + WallpaperEditorModal(chat) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt index 21009556a3..17280c248a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt @@ -118,7 +118,7 @@ enum class PredefinedBackgroundImage( ); fun toType(): BackgroundImageType = - BackgroundImageType.Repeated(filename, scale) + BackgroundImageType.Repeated(filename, 1f) companion object { fun from(filename: String): PredefinedBackgroundImage? = @@ -161,7 +161,9 @@ sealed class BackgroundImageType { @Serializable @SerialName("repeated") data class Repeated( override val filename: String, override val scale: Float, - ): BackgroundImageType() + ): BackgroundImageType() { + val predefinedImageScale = PredefinedBackgroundImage.from(filename)?.scale ?: 1f + } @Serializable @SerialName("static") data class Static( override val filename: String, @@ -193,11 +195,11 @@ sealed class BackgroundImageType { private var cachedImage: Pair? = null - fun from(wallpaper: ThemeWallpaper): BackgroundImageType? { - return if (wallpaper.preset != null) { - Repeated(wallpaper.preset, wallpaper.scale) + fun from(wallpaper: ThemeWallpaper?): BackgroundImageType? { + return if (wallpaper?.preset != null) { + Repeated(wallpaper.preset, wallpaper.scale ?: 1f) } else { - Static(wallpaper.imageFile ?: return null, wallpaper.scale, wallpaper.scaleType ?: BackgroundImageScaleType.FILL) + Static(wallpaper?.imageFile ?: return null, wallpaper.scale ?: 1f, wallpaper.scaleType ?: BackgroundImageScaleType.FILL) } } } @@ -220,7 +222,7 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageT drawRect(background) when (imageType) { - is BackgroundImageType.Repeated -> repeat(imageType.scale) + is BackgroundImageType.Repeated -> repeat(imageType.scale * imageType.predefinedImageScale) is BackgroundImageType.Static -> when (imageType.scaleType) { BackgroundImageScaleType.REPEAT -> repeat(imageType.scale) BackgroundImageScaleType.FILL, BackgroundImageScaleType.FIT -> { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 887a5bfdd9..6ee60c4596 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -41,9 +41,12 @@ enum class ModalPlacement { } class ModalData { - private val state = mutableMapOf>() + private val state = mutableMapOf>() fun stateGetOrPut (key: String, default: () -> T): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState + + fun stateGetOrPutNullable (key: String, default: () -> T?): MutableState = + state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState } class ModalManager(private val placement: ModalPlacement? = null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index 4d2e910cde..f20c4bb7ee 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -25,12 +25,15 @@ import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.ui.theme.isSystemInDarkTheme import chat.simplex.common.views.chat.item.PreviewChatItemView import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector import chat.simplex.res.MR +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString import java.net.URI @@ -63,6 +66,7 @@ object AppearanceScope { onValueChange = { val diff = it % 2.5f appPreferences.profileImageCornerRadius.set(it + (if (diff >= 1.25f) -diff + 2.5f else -diff)) + saveThemeToDatabase() }, colors = SliderDefaults.colors( activeTickColor = Color.Transparent, @@ -108,6 +112,8 @@ object AppearanceScope { fun WallpaperPresetSelector( selectedBackground: BackgroundImageType?, baseTheme: DefaultTheme, + initialBackgroundColor: Color?, + initialTintColor: Color?, onColorChange: (ThemeColor, Color?) -> Unit, onTypeChange: (BackgroundImageType?) -> Unit ) { @@ -143,7 +149,7 @@ object AppearanceScope { ) { if (background != null) { val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() } - ChatThemePreview(baseTheme, backgroundImage, background.toType(), withMessages = false) + ChatThemePreview(baseTheme, backgroundImage, background.toType(), withMessages = false, backgroundColor = initialBackgroundColor, tintColor = initialTintColor) } } } @@ -171,7 +177,7 @@ object AppearanceScope { contentAlignment = Alignment.Center ) { if (checked) { - ChatThemePreview(baseTheme, backgroundImage, type, withMessages = false) + ChatThemePreview(baseTheme, backgroundImage, type, withMessages = false, backgroundColor = initialBackgroundColor, tintColor = initialTintColor) } else { Plus() } @@ -185,11 +191,11 @@ object AppearanceScope { BackgroundItem(background) } item { - OwnBackgroundItem(MaterialTheme.wallpaper.type) + OwnBackgroundItem(selectedBackground) } } val backgroundImage = selectedBackground?.image - ChatThemePreview(baseTheme, backgroundImage, selectedBackground) + ChatThemePreview(baseTheme, backgroundImage, selectedBackground, backgroundColor = initialBackgroundColor, tintColor = initialTintColor) if (appPlatform.isDesktop) { val itemWidth = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 @@ -216,6 +222,15 @@ object AppearanceScope { } } + private var job: Job = Job() + private fun saveThemeToDatabase() { + job.cancel() + job = withBGApi { + delay(3000) + controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) + } + } + @Composable fun ThemesSection( systemDarkTheme: SharedPreference, @@ -230,12 +245,16 @@ object AppearanceScope { WallpaperPresetSelector( selectedBackground = backgroundImageType, baseTheme = currentTheme.base, + initialBackgroundColor = MaterialTheme.wallpaper.background, + initialTintColor = MaterialTheme.wallpaper.tint, onColorChange = { name, color -> ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + saveThemeToDatabase() }, onTypeChange = { type -> removeBackgroundImage(backgroundImageType?.filename) ThemeManager.saveAndApplyBackgroundImage(baseTheme, type) + saveThemeToDatabase() } ) @@ -243,10 +262,12 @@ object AppearanceScope { val state = remember { derivedStateOf { currentTheme.name } } ThemeSelector(state) { ThemeManager.applyTheme(it, darkTheme) + saveThemeToDatabase() } if (state.value == DefaultTheme.SYSTEM.themeName) { DarkThemeSelector(remember { systemDarkTheme.state }) { ThemeManager.changeDarkTheme(it, darkTheme) + saveThemeToDatabase() } } } @@ -278,66 +299,29 @@ object AppearanceScope { editColor, onColorChange = { name, color -> ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + saveThemeToDatabase() }, onTypeChange = { type -> removeBackgroundImage(backgroundImageType.filename) ThemeManager.saveAndApplyBackgroundImage(baseTheme, type) + saveThemeToDatabase() } ) } SectionDividerSpaced(maxTopPadding = true) } - SectionView(stringResource(MR.strings.theme_colors_section_title)) { - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { - val title = generalGetString(MR.strings.color_primary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { - val title = generalGetString(MR.strings.color_primary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { - val title = generalGetString(MR.strings.color_secondary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { - val title = generalGetString(MR.strings.color_secondary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { - val title = generalGetString(MR.strings.color_background) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.background) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { - val title = generalGetString(MR.strings.color_surface) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.surface) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { - val title = generalGetString(MR.strings.color_title) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { - val title = generalGetString(MR.strings.color_sent_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { - val title = generalGetString(MR.strings.color_received_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) - } + CustomizeThemeColorsSection(currentTheme) { name, color -> + editColor(name, color) + saveThemeToDatabase() } + val isInDarkTheme = isInDarkTheme() if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors, currentTheme.wallpaper)) { - SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { + SectionItemView({ + ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) + saveThemeToDatabase() + }) { Text(generalGetString(MR.strings.reset_color), color = colors.primary) } } @@ -353,7 +337,7 @@ object AppearanceScope { } } SectionItemView({ - val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) + val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme, null, chatModel.currentUser.value?.uiThemes) theme.value = yaml.encodeToString(overrides) withLongRunningApi { exportThemeLauncher.launch("simplex.theme")} }) { @@ -364,6 +348,7 @@ object AppearanceScope { val theme = getThemeFromUri(to) if (theme != null) { ThemeManager.saveAndApplyThemeOverrides(theme) + saveThemeToDatabase() } } } @@ -376,6 +361,57 @@ object AppearanceScope { } } + @Composable + fun CustomizeThemeColorsSection(currentTheme: ThemeManager.ActiveTheme, editColor: (ThemeColor, Color) -> Unit) { + SectionView(stringResource(MR.strings.theme_colors_section_title)) { + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { + val title = generalGetString(MR.strings.color_primary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.primary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { + val title = generalGetString(MR.strings.color_primary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.primaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { + val title = generalGetString(MR.strings.color_secondary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.secondary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { + val title = generalGetString(MR.strings.color_secondary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.secondaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { + val title = generalGetString(MR.strings.color_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.background) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { + val title = generalGetString(MR.strings.color_surface) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.surface) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { + val title = generalGetString(MR.strings.color_title) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { + val title = generalGetString(MR.strings.color_sent_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { + val title = generalGetString(MR.strings.color_received_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) + } + } + } + @Composable fun ColorEditor( name: ThemeColor, @@ -489,7 +525,7 @@ object AppearanceScope { private fun ThemeSelector(state: State, onSelected: (String) -> Unit) { val darkTheme = isSystemInDarkTheme() val values by remember(ChatController.appPrefs.appLanguage.state.value) { - mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) + mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.themeName to it.third }) } ExposedDropDownSettingRow( generalGetString(MR.strings.theme), @@ -538,6 +574,8 @@ fun WallpaperSetupView( WallpaperPresetSelector( selectedBackground = backgroundImageType, baseTheme = theme, + initialBackgroundColor = initialBackgroundColor, + initialTintColor = initialTintColor, onColorChange = onColorChange, onTypeChange = onTypeChange, ) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 31cdf452d9..465d078e16 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1550,7 +1550,14 @@ Repeat Fill Fit - Apply + Advanced settings + Reset to global theme + Set default theme + Apply to + all themes + light theme + dark theme + You allow