This commit is contained in:
Avently
2024-05-09 20:01:03 +07:00
parent fff76eb63f
commit 4475f0707c
12 changed files with 473 additions and 243 deletions
@@ -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<String, ThemeOverrides>? = 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<String, ThemeOverrides>? = null,
val uiThemes: ThemeModeOverrides? = null,
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
override val id get() = "#$groupId"
@@ -1156,7 +1156,7 @@ object ChatController {
return false
}
suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: Map<String, ThemeOverrides>?): 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<String, ThemeOverrides>?): CC()
class ApiSetChatUIThemes(val chatId: String, val themes: Map<String, ThemeOverrides>?): 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<String, ThemeOverrides>? = 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(),
)
}
@@ -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
@@ -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<String, ThemeOverrides>.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<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = 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) {
@@ -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<Colors, DefaultTheme> = 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<String, ThemeOverrides> = appPrefs.themeOverrides.get()): ActiveTheme {
fun currentColors(darkForSystemTheme: Boolean, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: Map<String, ThemeOverrides>): 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<Map<String, ThemeOverrides>> = 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<ThemeModeOverride>) {
val prevValue = pref.value
pref.value = prevValue.withUpdatedColor(name, color?.toReadableHex())
}
fun saveAndApplyBackgroundImage(baseTheme: DefaultTheme, type: BackgroundImageType?, pref: SharedPreference<Map<String, ThemeOverrides>> = 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<ThemeModeOverride>) {
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<Map<String, ThemeOverrides>> = 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<Map<String, ThemeOverrides>> = 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 =
@@ -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<BackgroundImageType?> = remember { derivedStateOf { BackgroundImageType.from(themeOverrides.value.wallpaper) } }
val pref = remember {
SharedPreference<Map<String, ThemeOverrides>>(
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<BackgroundImageType?> = 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
)
}
@@ -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,
@@ -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)
}
}
}
@@ -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<String, ImageBitmap>? = 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 -> {
@@ -41,9 +41,12 @@ enum class ModalPlacement {
}
class ModalData {
private val state = mutableMapOf<String, MutableState<Any>>()
private val state = mutableMapOf<String, MutableState<Any?>>()
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
}
class ModalManager(private val placement: ModalPlacement? = null) {
@@ -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<String?>,
@@ -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<ThemeOverrides>(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<String>, 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,
)
@@ -1550,7 +1550,14 @@
<string name="background_image_scale_repeat">Repeat</string>
<string name="background_image_scale_fill">Fill</string>
<string name="background_image_scale_fit">Fit</string>
<string name="background_image_apply">Apply</string>
<string name="background_image_advanced_settings">Advanced settings</string>
<string name="chat_theme_reset_to_global_theme">Reset to global theme</string>
<string name="chat_theme_set_default_theme">Set default theme</string>
<string name="chat_theme_apply_to_mode">Apply to</string>
<string name="chat_theme_apply_to_all_modes">all themes</string>
<string name="chat_theme_apply_to_light_mode">light theme</string>
<string name="chat_theme_apply_to_dark_mode">dark theme</string>
<!-- Preferences.kt -->
<string name="chat_preferences_you_allow">You allow</string>