From 572dcb15569083db67c954303c256c524b499c08 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Thu, 2 May 2024 18:26:19 +0700 Subject: [PATCH] backgrounds moved to themes. can be exported/imported --- .../chat/simplex/common/model/ChatModel.kt | 2 - .../chat/simplex/common/model/SimpleXAPI.kt | 13 -- .../chat/simplex/common/ui/theme/Theme.kt | 114 ++++++++++++++---- .../simplex/common/ui/theme/ThemeManager.kt | 39 ++++-- .../simplex/common/views/chat/ChatView.kt | 4 +- .../views/helpers/ChatViewBackground.kt | 84 +++++++++---- .../simplex/common/views/helpers/Utils.kt | 48 +++----- .../common/views/usersettings/Appearance.kt | 89 ++++++++------ .../commonMain/resources/MR/base/strings.xml | 6 +- 9 files changed, 245 insertions(+), 154 deletions(-) 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 81cedf3a5e..671bf25c58 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 @@ -137,8 +137,6 @@ object ChatModel { val processedCriticalError: ProcessedErrors = ProcessedErrors(60_000) val processedInternalError: ProcessedErrors = ProcessedErrors(20_000) - val backgroundImage: MutableState by lazy { mutableStateOf(getBackgroundImageOrDefault()) } - fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { 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 43a4f46216..73fc9d16a4 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 @@ -175,18 +175,6 @@ class AppPreferences { json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) }, settingsThemes) val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f) - private val _backgroundImageType = mkStrPreference(SHARED_PREFS_BACKGROUND_IMAGE, null) - val backgroundImageType: SharedPreference = SharedPreference( - get = fun(): BackgroundImageType? { - val value = _backgroundImageType.get() ?: return null - return try { - json.decodeFromString(value) - } catch (e: Throwable) { - null - } - }, - set = fun(type: BackgroundImageType?) { _backgroundImageType.set(json.encodeToString(type)) } - ) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) @@ -354,7 +342,6 @@ class AppPreferences { private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" private const val SHARED_PREFS_THEMES = "Themes" private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius" - private const val SHARED_PREFS_BACKGROUND_IMAGE = "BackgroundImage" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" 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 6ebfee6a88..d380f84a86 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 @@ -6,10 +6,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController -import chat.simplex.common.platform.isInNightMode +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.views.helpers.* @@ -17,12 +16,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import chat.simplex.res.MR +import java.io.File enum class DefaultTheme { SYSTEM, LIGHT, DARK, SIMPLEX; // Call it only with base theme, not SYSTEM - fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean { + fun hasChangedAnyColor(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Boolean { val palette = when (this) { SYSTEM -> return false LIGHT -> LightColorPalette @@ -41,7 +41,9 @@ enum class DefaultTheme { colors.secondaryVariant != palette.secondaryVariant || colors.background != palette.background || colors.surface != palette.surface || - appColors != appPalette + appColors != appPalette || + appWallpaper.background != null || + appWallpaper.tint != null } } @@ -49,14 +51,18 @@ data class AppColors( val title: Color, val sentMessage: Color, val receivedMessage: Color, - val wallpaperBackground: Color?, - val wallpaperTint: Color?, +) + +data class AppWallpaper( + val background: Color? = null, + val tint: Color? = null, + val type: BackgroundImageType? = null ) enum class ThemeColor { PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE, WALLPAPER_BACKGROUND, WALLPAPER_TINT; - fun fromColors(colors: Colors, appColors: AppColors): Color? { + fun fromColors(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Color? { return when (this) { PRIMARY -> colors.primary PRIMARY_VARIANT -> colors.primaryVariant @@ -67,8 +73,8 @@ enum class ThemeColor { TITLE -> appColors.title SENT_MESSAGE -> appColors.sentMessage RECEIVED_MESSAGE -> appColors.receivedMessage - WALLPAPER_BACKGROUND -> appColors.wallpaperBackground - WALLPAPER_TINT -> appColors.wallpaperTint + WALLPAPER_BACKGROUND -> appWallpaper.background + WALLPAPER_TINT -> appWallpaper.tint } } @@ -102,8 +108,6 @@ data class ThemeColors( val title: String? = null, val sentMessage: String? = null, val receivedMessage: String? = null, - val wallpaperBackground: String? = null, - val wallpaperTint: String? = null, ) { fun toColors(base: DefaultTheme): Colors { val baseColors = when (base) { @@ -135,8 +139,6 @@ data class ThemeColors( title = title?.colorFromReadableHex() ?: baseColors.title, sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, - wallpaperBackground = wallpaperBackground?.colorFromReadableHex() ?: baseColors.wallpaperBackground, - wallpaperTint = wallpaperTint?.colorFromReadableHex() ?: baseColors.wallpaperTint, ) } @@ -153,16 +155,79 @@ data class ThemeColors( title = ac.title.toReadableHex(), sentMessage = ac.sentMessage.toReadableHex(), receivedMessage = ac.receivedMessage.toReadableHex(), - wallpaperBackground = ac.wallpaperBackground?.toReadableHex(), - wallpaperTint = ac.wallpaperTint?.toReadableHex(), ) } } +@Serializable +data class ThemeWallpaper ( + val preset: String? = null, + val scale: Float = 1f, + val scaleType: BackgroundImageScaleType? = null, + val background: String? = null, + val tint: String? = null, + val image: String? = null, +) { + fun toAppWallpaper(): AppWallpaper { + return AppWallpaper( + background = background?.colorFromReadableHex(), + tint = tint?.colorFromReadableHex(), + type = if (preset != null) { + BackgroundImageType.Repeated(filename = preset, scale) + } else if (image != null) { + BackgroundImageType.Static( + filename = image, + scale, + scaleType ?: BackgroundImageScaleType.FILL + ) + } else + null + ) + } + + fun withFilledWallpaper(): ThemeWallpaper { + val aw = toAppWallpaper() + return ThemeWallpaper( + image = if (aw.type is BackgroundImageType.Static) resizeImageToStrSize(aw.type.image, 5_000_000) else null, + preset = if (aw.type is BackgroundImageType.Repeated) aw.type.filename else null, + scale = if (aw.type is BackgroundImageType.Repeated) aw.type.scale else (aw.type as BackgroundImageType.Static).scale, + scaleType = if (aw.type is BackgroundImageType.Static) aw.type.scaleType else null, + background = aw.background?.toReadableHex(), + tint = aw.tint?.toReadableHex(), + ) + } + + fun import(): ThemeWallpaper = + if (preset == null && image != null) { + // Need to save image from string and to save its path + try { + val parsed = base64ToBitmap(image) + val filename = saveBackgroundImage(parsed) + copy(image = filename) + } catch (e: Exception) { + ThemeWallpaper() + } + } else this + + companion object { + fun from(type: BackgroundImageType, background: String?, tint: String?): ThemeWallpaper { + return ThemeWallpaper( + image = if (type is BackgroundImageType.Static) type.filename else null, + preset = if (type is BackgroundImageType.Repeated) type.filename else null, + scale = if (type is BackgroundImageType.Repeated) type.scale else (type as BackgroundImageType.Static).scale, + scaleType = if (type is BackgroundImageType.Static) type.scaleType else null, + background = background, + tint = tint, + ) + } + } +} + @Serializable data class ThemeOverrides ( val base: DefaultTheme, - val colors: ThemeColors + val colors: ThemeColors, + val wallpaper: ThemeWallpaper = ThemeWallpaper(), ) { fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides { return copy(colors = when (name) { @@ -175,9 +240,14 @@ data class ThemeOverrides ( 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(wallpaperBackground = color) - ThemeColor.WALLPAPER_TINT -> colors.copy(wallpaperTint = 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() + } + ) } } @@ -225,8 +295,6 @@ val DarkColorPaletteApp = AppColors( title = SimplexBlue, sentMessage = SentMessageColor, receivedMessage = Color(0x20B1B0B5), - wallpaperBackground = null, - wallpaperTint = null, ) val LightColorPalette = lightColors( @@ -246,8 +314,6 @@ val LightColorPaletteApp = AppColors( title = SimplexBlue, sentMessage = SentMessageColor, receivedMessage = Color(0x20B1B0B5), - wallpaperBackground = null, - wallpaperTint = null, ) val SimplexColorPalette = darkColors( @@ -268,8 +334,6 @@ val SimplexColorPaletteApp = AppColors( title = Color(0xFF267BE5), sentMessage = SentMessageColor, receivedMessage = Color(0x20B1B0B5), - wallpaperBackground = null, - wallpaperTint = null, ) val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) 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 0419599c0c..c57a124a36 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 @@ -8,6 +8,7 @@ import chat.simplex.res.MR import chat.simplex.common.model.AppPreferences import chat.simplex.common.model.ChatController import chat.simplex.common.platform.platform +import chat.simplex.common.views.helpers.BackgroundImageType import chat.simplex.common.views.helpers.generalGetString // https://github.com/rsms/inter @@ -18,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) + data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper) private fun systemDarkThemeColors(): Pair = when (appPrefs.systemDarkTheme.get()) { DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK @@ -43,9 +44,9 @@ object ThemeManager { else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) } if (theme == null) { - return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third) + return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third, AppWallpaper()) } - return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) + return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base), theme.wallpaper.toAppWallpaper()) } fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { @@ -56,8 +57,8 @@ object ThemeManager { if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name } val overrides = appPrefs.themeOverrides.get().toMutableMap() - val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base)) + val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) + return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base), wallpaper = nonFilledTheme.wallpaper.withFilledWallpaper()) } // colors, default theme enum, localized name of theme @@ -116,24 +117,38 @@ object ThemeManager { if (colorToSet == null) { // Setting default color from a base theme colorToSet = when(nonSystemThemeName) { - DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp) + DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp, AppWallpaper()) + DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp, AppWallpaper()) + DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper()) // Will not be here else -> return } } val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) + val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet?.toReadableHex()) appPrefs.themeOverrides.set(overrides) CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } + fun saveAndApplyBackgroundImage(type: BackgroundImageType?, darkForSystemTheme: Boolean) { + val themeName = appPrefs.currentTheme.get()!! + val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + themeName + } else { + if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + } + val overrides = appPrefs.themeOverrides.get().toMutableMap() + val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) + overrides[nonSystemThemeName] = prevValue.copy(wallpaper = if (type != null) ThemeWallpaper.from(type, prevValue.wallpaper.background, prevValue.wallpaper.tint) else ThemeWallpaper()) + appPrefs.themeOverrides.set(overrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + } + fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - overrides[theme.base.name] = prevValue.copy(colors = theme.colors) + val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) + overrides[theme.base.name] = prevValue.copy(colors = theme.colors, wallpaper = theme.wallpaper.import()) appPrefs.themeOverrides.set(overrides) appPrefs.currentTheme.set(theme.base.name) CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) @@ -148,7 +163,7 @@ object ThemeManager { } val overrides = appPrefs.themeOverrides.get().toMutableMap() val prevValue = overrides[nonSystemThemeName] ?: return - overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors()) + overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper.copy(background = null, tint = null)) appPrefs.themeOverrides.set(overrides) CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } 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 58336ca33d..5ba47a390f 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 @@ -597,8 +597,8 @@ fun ChatLayout( drawerContentColor = LocalContentColor.current, backgroundColor = Color.Unspecified ) { contentPadding -> - val backgroundImage = remember { chatModel.backgroundImage }.value - val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type val defaultBackgroundColor = backgroundImageType?.defaultBackgroundColor val defaultTintColor = backgroundImageType?.defaultTintColor BoxWithConstraints(Modifier 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 55a45d16a2..4cdcb58d64 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 @@ -9,23 +9,28 @@ import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.io.File import kotlin.math.* @Serializable -enum class PredefinedBackgroundImage(val res: ImageResource, val filename: String, val text: StringResource, val type: BackgroundImageType) { - @SerialName("cat") CAT(MR.images.background_cat, "simplex_cat", MR.strings.background_cat, BackgroundImageType.Repeated("simplex_cat", 0.5f)), - @SerialName("hearts") HEARTS(MR.images.background_hearts, "simplex_hearts", MR.strings.background_hearts, BackgroundImageType.Repeated("simplex_hearts", 0.5f)), - @SerialName("school") SCHOOL(MR.images.background_school, "simplex_school", MR.strings.background_school, BackgroundImageType.Repeated("simplex_school", 0.5f)), - @SerialName("internet") INTERNET(MR.images.background_internet, "simplex_internet", MR.strings.background_internet, BackgroundImageType.Repeated("simplex_internet", 0.5f)), - @SerialName("space") SPACE(MR.images.background_space, "simplex_space", MR.strings.background_space, BackgroundImageType.Repeated("simplex_space", 0.5f)), - @SerialName("pets") PETS(MR.images.background_pets, "simplex_pets", MR.strings.background_pets, BackgroundImageType.Repeated("simplex_pets", 0.5f)), - @SerialName("rabbit") RABBIT(MR.images.background_rabbit, "simplex_rabbit", MR.strings.background_rabbit, BackgroundImageType.Repeated("simplex_rabbit", 0.5f)); +enum class PredefinedBackgroundImage(val res: ImageResource, val filename: String, val scale: Float, val text: StringResource) { + @SerialName("cat") CAT(MR.images.background_cat, "simplex_cat", 0.5f, MR.strings.background_cat), + @SerialName("hearts") HEARTS(MR.images.background_hearts, "simplex_hearts", 0.5f, MR.strings.background_hearts), + @SerialName("school") SCHOOL(MR.images.background_school, "simplex_school", 0.5f, MR.strings.background_school), + @SerialName("internet") INTERNET(MR.images.background_internet, "simplex_internet", 0.5f, MR.strings.background_internet), + @SerialName("space") SPACE(MR.images.background_space, "simplex_space", 0.5f, MR.strings.background_space), + @SerialName("pets") PETS(MR.images.background_pets, "simplex_pets", 0.5f, MR.strings.background_pets), + @SerialName("rabbit") RABBIT(MR.images.background_rabbit, "simplex_rabbit", 0.5f, MR.strings.background_rabbit); + + fun toType(): BackgroundImageType = + BackgroundImageType.Repeated(filename, scale) companion object { fun from(filename: String): PredefinedBackgroundImage? = @@ -34,17 +39,31 @@ enum class PredefinedBackgroundImage(val res: ImageResource, val filename: Strin } @Serializable -enum class BackgroundImageScale(val contentScale: ContentScale, val text: StringResource) { - @SerialName("crop") CROP(ContentScale.Crop, MR.strings.background_image_scale_crop), +enum class BackgroundImageScaleType(val contentScale: ContentScale, val text: StringResource) { + @SerialName("fill") FILL(ContentScale.Crop, MR.strings.background_image_scale_fill), @SerialName("fit") FIT(ContentScale.Fit, MR.strings.background_image_scale_fit), - @SerialName("fillWidth") FILL_WIDTH(ContentScale.FillWidth, MR.strings.background_image_scale_fill_width), - @SerialName("fillHeight") FILL_HEIGHT(ContentScale.FillHeight, MR.strings.background_image_scale_fill_height), - @SerialName("fillBounds") FILL_BOUNDS(ContentScale.FillBounds, MR.strings.background_image_scale_fill_bounds) + @SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.background_image_scale_repeat), } @Serializable sealed class BackgroundImageType { abstract val filename: String + + val image by lazy { + val cache = cachedImage + if (cache != null && cache.first == filename) { + cache.second + } else { + val res = if (this is Repeated) { + PredefinedBackgroundImage.from(filename)!!.res.toComposeImageBitmap()!! + } else { + File(getBackgroundImageFilePath(filename)).inputStream().use { loadImageBitmap(it) } + } + cachedImage = filename to res + res + } + } + @Serializable @SerialName("repeated") data class Repeated( override val filename: String, val scale: Float, @@ -52,34 +71,40 @@ sealed class BackgroundImageType { @Serializable @SerialName("static") data class Static( override val filename: String, - val scale: BackgroundImageScale, + val scale: Float, + val scaleType: BackgroundImageScaleType, ): BackgroundImageType() val background: Color? - get() = CurrentColors.value.appColors.wallpaperBackground + get() = CurrentColors.value.wallpaper.background val tint: Color? - get() = CurrentColors.value.appColors.wallpaperTint - - fun toPredefined(): PredefinedBackgroundImage? = PredefinedBackgroundImage.from(filename) + get() = CurrentColors.value.wallpaper.tint val defaultBackgroundColor: Color - @Composable get() = if (this is Static) MaterialTheme.colors.background else MaterialTheme.colors.background + @Composable get() = if (this is Static) + MaterialTheme.colors.background + else + MaterialTheme.colors.background val defaultTintColor: Color - @Composable get() = if (this is Static) MaterialTheme.colors.background.copy(0.9f) else MaterialTheme.colors.primary + @Composable get() = if (this is Repeated || (this is Static && this.scaleType == BackgroundImageScaleType.REPEAT)) + MaterialTheme.colors.primary + else + MaterialTheme.colors.background.copy(0.9f) companion object { - val default: BackgroundImageType = - Repeated(PredefinedBackgroundImage.CAT.filename, 1f) + val default: BackgroundImageType + get() = PredefinedBackgroundImage.CAT.toType() + + private var cachedImage: Pair? = null } } fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageType, defaultBackground: Color, defaultTint: Color) = clipRect { - drawRect(imageType.background ?: defaultBackground) - if (imageType is BackgroundImageType.Repeated) { - val scale = imageType.scale * density + fun repeat(imageScale: Float) { + val scale = imageScale * density for (h in 0..(size.height / image.height / scale).roundToInt()) { for (w in 0..(size.width / image.width / scale).roundToInt()) { drawImage( @@ -90,8 +115,15 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageT ) } } + } + + drawRect(imageType.background ?: defaultBackground) + if (imageType is BackgroundImageType.Repeated) { + repeat(imageType.scale) + } else if (imageType is BackgroundImageType.Static && imageType.scaleType == BackgroundImageScaleType.REPEAT) { + repeat(imageType.scale) } else if (imageType is BackgroundImageType.Static) { - val scale = imageType.scale.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) + val scale = imageType.scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) val scaledWidth = (image.width * scale.scaleX).roundToInt() val scaledHeight = (image.height * scale.scaleY).roundToInt() drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index fc62cbc356..2d1b2f9c90 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -160,34 +160,6 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri return null } -fun getBackgroundImageFromUri(uri: URI, withAlertOnException: Boolean = true): Pair? { - uri.inputStream().use { - runCatching { - return uri.toFile().name to loadImageBitmap(it!!) - }.onFailure { - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.import_background_image_error), - text = generalGetString(MR.strings.import_background_image_desc), - ) - } - } - } - return null -} - -fun getBackgroundImageOrDefault(filename: String? = null): ImageBitmap? { - val type = appPreferences.backgroundImageType.get() ?: return null - val res = if (type is BackgroundImageType.Static) { - File(getBackgroundImageFilePath(type.filename)).inputStream().use { - loadImageBitmap(it) - } - } else { - PredefinedBackgroundImage.from(type.filename)?.res?.toComposeImageBitmap() - } - return res ?: BackgroundImageType.default.toPredefined()!!.res.toComposeImageBitmap()!! -} - fun saveImage(uri: URI): CryptoFile? { val bitmap = getBitmapFromUri(uri) ?: return null return saveImage(bitmap) @@ -312,9 +284,9 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile? } } -fun saveBackgroundImage(uri: URI): Pair? { - val res = getBackgroundImageFromUri(uri, true) ?: return null - val destFile = File(getBackgroundImageFilePath(res.first)) +fun saveBackgroundImage(uri: URI): String? { + val destFileName = generateNewFileName("background", "jpg", File(getBackgroundImageFilePath(""))) + val destFile = File(getBackgroundImageFilePath(destFileName)) val inputStream = uri.inputStream() try { Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) @@ -322,9 +294,21 @@ fun saveBackgroundImage(uri: URI): Pair? { Log.e(TAG, "Error saving background image: ${e.stackTraceToString()}") return null } - return res + return destFile.name } +fun saveBackgroundImage(image: ImageBitmap): String { + val destFileName = generateNewFileName("background", "jpg", File(getBackgroundImageFilePath(""))) + val destFile = File(getBackgroundImageFilePath(destFileName)) + val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000) + val output = FileOutputStream(destFile) + dataResized.use { + it.writeTo(output) + } + return destFile.name +} + +// TODO: removes all background for all themes. Should remove only unused fun removeBackgroundImages(except: String? = null) { File(getBackgroundImageFilePath("_")).parentFile.listFiles()?.forEach { if (it.name != except) it.delete() 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 3e0ef84148..a5c80d9bee 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 @@ -23,7 +23,6 @@ import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel @@ -31,7 +30,6 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.views.chat.item.PreviewChatItemView import chat.simplex.res.MR -import com.godaddy.android.colorpicker.* import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString import java.net.URI @@ -106,8 +104,8 @@ object AppearanceScope { ) { AppBarTitle(stringResource(MR.strings.choose_background_image_title)) - val backgroundImage = remember { chatModel.backgroundImage }.value - val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type if (backgroundImage != null && backgroundImageType != null) { ChatThemePreview(backgroundImage, backgroundImageType) @@ -123,14 +121,13 @@ object AppearanceScope { val imageTypeValues = remember { PredefinedBackgroundImage.entries.map { it.filename to generalGetString(it.text) } + ("" to generalGetString(MR.strings.background_choose_own_image)) } + val systemDark = isSystemInDarkTheme() val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) { - val res = saveBackgroundImage(to) - if (res != null) { - val (filename, backgroundImage) = res + val filename = saveBackgroundImage(to) + if (filename != null) { imageTypeState.value = "" - chatModel.backgroundImage.value = backgroundImage - appPrefs.backgroundImageType.set(BackgroundImageType.Static(filename, BackgroundImageScale.CROP)) + ThemeManager.saveAndApplyBackgroundImage(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL), systemDark) removeBackgroundImages(filename) resetColors() } @@ -145,8 +142,7 @@ object AppearanceScope { withLongRunningApi { importBackgroundImageLauncher.launch("image/*") } } else { imageTypeState.value = filename - appPrefs.backgroundImageType.set(PredefinedBackgroundImage.from(filename)!!.type) - chatModel.backgroundImage.value = getBackgroundImageOrDefault() + ThemeManager.saveAndApplyBackgroundImage(PredefinedBackgroundImage.from(filename)!!.toType(), systemDark) removeBackgroundImages() } } @@ -159,23 +155,37 @@ object AppearanceScope { state.value, valueRange = 0.2f..2f, onValueChange = { - appPrefs.backgroundImageType.set(backgroundImageType.copy(scale = it)) + ThemeManager.saveAndApplyBackgroundImage(backgroundImageType.copy(scale = it), systemDark) } ) } } else if (backgroundImageType is BackgroundImageType.Static) { - val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) } + val state = remember(backgroundImageType.scaleType) { mutableStateOf(backgroundImageType.scaleType) } val values = remember { - BackgroundImageScale.entries.map { it to generalGetString(it.text) } + BackgroundImageScaleType.entries.map { it to generalGetString(it.text) } } ExposedDropDownSettingRow( stringResource(MR.strings.background_image_scale), values, state, - onSelected = { scale -> - appPrefs.backgroundImageType.set(backgroundImageType.copy(scale = scale)) + onSelected = { scaleType -> + ThemeManager.saveAndApplyBackgroundImage(backgroundImageType.copy(scaleType = scaleType), systemDark) } ) + + if (backgroundImageType.scaleType == BackgroundImageScaleType.REPEAT) { + val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) } + Row { + Text("${state.value}", Modifier.width(50.dp)) + Slider( + state.value, + valueRange = 0.2f..2f, + onValueChange = { + ThemeManager.saveAndApplyBackgroundImage(backgroundImageType.copy(scale = it), systemDark) + } + ) + } + } } SectionSpacer() @@ -245,12 +255,16 @@ object AppearanceScope { editColor: (ThemeColor, Color) -> Unit ) { val currentTheme by CurrentColors.collectAsState() + val systemDark = isSystemInDarkTheme() SectionView(stringResource(MR.strings.settings_section_title_themes)) { - val selectedBackground = remember { appPrefs.backgroundImageType.state }.value + val selectedBackground = CurrentColors.collectAsState().value.wallpaper.type val cornerRadius = remember { appPreferences.profileImageCornerRadius.state } fun setBackground(type: BackgroundImageType?) { - appPrefs.backgroundImageType.set(type) - chatModel.backgroundImage.value = getBackgroundImageOrDefault() + if (type is BackgroundImageType.Static) { + ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, null, systemDark) + ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, null, systemDark) + } + ThemeManager.saveAndApplyBackgroundImage(type, systemDark) removeBackgroundImages(type?.filename) } @Composable @@ -272,12 +286,12 @@ object AppearanceScope { Modifier .size(width, height) .background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt())) - .clickable { setBackground(background?.type) }, + .clickable { setBackground(background?.toType()) }, contentAlignment = Alignment.Center ) { if (background != null) { val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() } - ChatThemePreview(backgroundImage, background.type, withMessages = false) + ChatThemePreview(backgroundImage, background.toType(), withMessages = false) } if ((background == null && selectedBackground == null) || (selectedBackground?.filename == background?.filename)) { Checked() @@ -289,10 +303,9 @@ object AppearanceScope { fun OwnBackgroundItem(type: BackgroundImageType?) { val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) { - val res = saveBackgroundImage(to) - if (res != null) { - val (filename, backgroundImage) = res - setBackground(BackgroundImageType.Static(filename, BackgroundImageScale.CROP)) + val filename = saveBackgroundImage(to) + if (filename != null) { + setBackground(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL)) } } } @@ -305,7 +318,7 @@ object AppearanceScope { }, contentAlignment = Alignment.Center ) { - val backgroundImage = remember { chatModel.backgroundImage }.value + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image if (type is BackgroundImageType.Static && backgroundImage != null) { ChatThemePreview(backgroundImage, type, withMessages = false) Checked() @@ -322,7 +335,7 @@ object AppearanceScope { BackgroundItem(background) } item { - OwnBackgroundItem(remember { appPrefs.backgroundImageType.state }.value) + OwnBackgroundItem(CurrentColors.collectAsState().value.wallpaper.type) } } if (appPlatform.isDesktop) { @@ -341,11 +354,11 @@ object AppearanceScope { } else { LazyHorizontalGrid( rows = GridCells.Fixed(1), - Modifier.height(70.dp + DEFAULT_PADDING * 2), + Modifier.height(120.dp + DEFAULT_PADDING * 2), contentPadding = PaddingValues(DEFAULT_PADDING), horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), ) { - gridContent(50.dp, 70.dp) + gridContent(80.dp, 120.dp) } } @@ -375,20 +388,20 @@ object AppearanceScope { AppBarTitle(stringResource(MR.strings.customize_theme_title)) - val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value - val backgroundImage = remember { chatModel.backgroundImage } - ChatThemePreview(backgroundImage.value, backgroundImageType) + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type + ChatThemePreview(backgroundImage, backgroundImageType) SectionSpacer() if (backgroundImageType != null) { SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) { - val wallpaperBackgroundColor = currentTheme.appColors.wallpaperBackground ?: backgroundImageType.defaultBackgroundColor + val wallpaperBackgroundColor = currentTheme.wallpaper.background ?: backgroundImageType.defaultBackgroundColor SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND, wallpaperBackgroundColor) }) { val title = generalGetString(MR.strings.color_wallpaper_background) Text(title) Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor) } - val wallpaperTintColor = currentTheme.appColors.wallpaperTint ?: backgroundImageType.defaultTintColor + val wallpaperTintColor = currentTheme.wallpaper.tint ?: backgroundImageType.defaultTintColor SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT, wallpaperTintColor) }) { val title = generalGetString(MR.strings.color_wallpaper_tint) Text(title) @@ -446,7 +459,7 @@ object AppearanceScope { } } val isInDarkTheme = isInDarkTheme() - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { + if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors, currentTheme.wallpaper)) { SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { Text(generalGetString(MR.strings.reset_color), color = colors.primary) } @@ -500,9 +513,9 @@ object AppearanceScope { val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) if (supportedLiveChange) { - val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value - val backgroundImage = remember { chatModel.backgroundImage } - ChatThemePreview(backgroundImage.value, backgroundImageType) + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type + ChatThemePreview(backgroundImage, backgroundImageType) SectionSpacer() } 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 ecbb79e34b..5e4b498288 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1551,11 +1551,9 @@ Background Tint Scale - Crop + Repeat + Fill Fit - Fill width - Fill height - Fill bounds You allow