diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 6f7f6759ba..1951d65715 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -58,6 +58,7 @@ enum class AppIcon(val image: ImageResource) { @Composable actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { val appIcon = remember { mutableStateOf(findEnabledIcon()) } + val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() fun setAppIcon(newIcon: AppIcon) { if (appIcon.value == newIcon) return @@ -76,6 +77,9 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod appIcon.value = newIcon } + val theme = CurrentColors.collectAsState().value.base + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type AppearanceScope.AppearanceLayout( appIcon, m.controller.appPrefs.appLanguage, @@ -84,7 +88,7 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod showSettingsModal = showSettingsModal, editColor = { name, initialColor -> ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, close) + ColorEditor(name, initialColor, theme, backgroundImageType, backgroundImage, onColorChange = { color -> ThemeManager.saveAndApplyThemeColor(name, color, darkTheme) }, close = close) } }, ) 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 d380f84a86..a7ca8420d7 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 @@ -16,7 +16,6 @@ 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; @@ -197,7 +196,7 @@ data class ThemeWallpaper ( ) } - fun import(): ThemeWallpaper = + fun importFromString(): ThemeWallpaper = if (preset == null && image != null) { // Need to save image from string and to save its path try { 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 c57a124a36..bc4d0374bb 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 @@ -4,9 +4,8 @@ import androidx.compose.material.Colors 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.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 @@ -27,9 +26,9 @@ object ThemeManager { else -> SimplexColorPalette to DefaultTheme.SIMPLEX } - fun currentColors(darkForSystemTheme: Boolean): ActiveTheme { + fun currentColors(darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides): ActiveTheme { val themeName = appPrefs.currentTheme.get()!! - val themeOverrides = appPrefs.themeOverrides.get() + val themeOverrides = pref.get() val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { themeName @@ -97,16 +96,16 @@ object ThemeManager { fun applyTheme(theme: String, darkForSystemTheme: Boolean) { appPrefs.currentTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme) + CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides) platform.androidSetNightModeIfSupported() } fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { appPrefs.systemDarkTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme) + CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides) } - fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) { + fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { themeName @@ -124,48 +123,48 @@ object ThemeManager { else -> return } } - val overrides = appPrefs.themeOverrides.get().toMutableMap() + val overrides = pref.get().toMutableMap() 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) + pref.set(overrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) } - fun saveAndApplyBackgroundImage(type: BackgroundImageType?, darkForSystemTheme: Boolean) { + fun saveAndApplyBackgroundImage(type: BackgroundImageType?, darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { 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 overrides = pref.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) + pref.set(overrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) } - fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { - val overrides = appPrefs.themeOverrides.get().toMutableMap() + fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { + val overrides = pref.get().toMutableMap() 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) + overrides[theme.base.name] = prevValue.copy(colors = theme.colors, wallpaper = theme.wallpaper.importFromString()) + pref.set(overrides) appPrefs.currentTheme.set(theme.base.name) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) } - fun resetAllThemeColors(darkForSystemTheme: Boolean) { + fun resetAllThemeColors(darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { 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 overrides = pref.get().toMutableMap() val prevValue = overrides[nonSystemThemeName] ?: return 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) + pref.set(overrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) } fun String.colorFromReadableHex(): Color = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index dcd36e026b..dfa713f414 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -29,11 +29,13 @@ 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.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex import chat.simplex.common.views.chatlist.updateChatSettings import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @@ -337,6 +339,16 @@ fun ChatInfoLayout( if (cStats != null && cStats.ratchetSyncAllowed) { SynchronizeConnectionButton(syncContactConnection) } + // Should come from API + val type = remember { BackgroundImageType.default } + val theme = remember { ThemeOverrides(CurrentColors.value.base, ThemeColors()) } + WallpaperButton { + ModalManager.end.showModal { + WallpaperEditor(type, theme) { type, theme -> + // apply to chat + } + } + } // } else if (developerTools) { // SynchronizeConnectionButtonForce(syncContactConnectionForce) // } @@ -642,6 +654,15 @@ private fun SendReceiptsOption(currentUser: User, state: State, on ) } +@Composable +fun WallpaperButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_image), + stringResource(MR.strings.settings_section_title_wallpaper), + click = onClick + ) +} + @Composable fun ClearChatButton(onClick: () -> Unit) { SettingsActionItem( @@ -675,6 +696,88 @@ fun ShareAddressButton(onClick: () -> Unit) { ) } +@Composable +fun ModalData.WallpaperEditor(type: BackgroundImageType, theme: ThemeOverrides, save: (BackgroundImageType?, ThemeOverrides) -> Unit) { + ColumnWithScrollBar( + Modifier + .fillMaxSize() + ) { + val systemDark = chat.simplex.common.ui.theme.isSystemInDarkTheme() + val backgroundImageType: MutableState = remember { stateGetOrPut("backgroundImageType") { type } } + val background = backgroundImageType.value + val themeOverrides = remember { stateGetOrPut("themeOverrides") { theme } } + val pref = remember { + SharedPreference>( + get = { + mapOf(CurrentColors.value.base.name to themeOverrides.value) + }, + set = { value -> + themeOverrides.value = value[CurrentColors.value.base.name]!! + } + ) + } + + AppBarTitle(stringResource(MR.strings.settings_section_title_wallpaper)) + val backgroundImage = remember(background?.filename) { background?.image } + val backgroundColor = remember { mutableStateOf(pref.get()[CurrentColors.value.base.name]!!.wallpaper.background?.colorFromReadableHex()) } + val tintColor = remember { mutableStateOf(pref.get()[CurrentColors.value.base.name]!!.wallpaper.tint?.colorFromReadableHex()) } + + AppearanceScope.ChatThemePreview(theme.base, backgroundImage, background, backgroundColor.value, tintColor.value) + SectionSpacer() + + WallpaperSetupView( + background, + theme.base, + backgroundColor.value, + tintColor.value, + showPresetSelection = true, + editColor = { name, initialColor -> + ModalManager.end.showModalCloseable { close -> + AppearanceScope.ColorEditor( + name, + initialColor, + theme.base, + backgroundImageType.value, + backgroundImage, + backgroundColor.value, + tintColor.value, + onColorChange = { color -> + ThemeManager.saveAndApplyThemeColor(name, color, systemDark, pref) + if (name == ThemeColor.WALLPAPER_BACKGROUND) backgroundColor.value = color + else if (name == ThemeColor.WALLPAPER_TINT) tintColor.value = color + }, + close + ) + } + }, + onColorChange = { name, color -> + ThemeManager.saveAndApplyThemeColor(name, color, systemDark, pref) + if (name == ThemeColor.WALLPAPER_BACKGROUND) backgroundColor.value = color + else if (name == ThemeColor.WALLPAPER_TINT) tintColor.value = color + }, + onTypeChange = { type -> + ThemeManager.saveAndApplyBackgroundImage(type, systemDark, pref) + backgroundImageType.value = type + } + ) + + SectionSpacer() + + ApplyButton { + save(backgroundImageType.value, themeOverrides.value) + } + } +} + +@Composable +private fun ApplyButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_check), + stringResource(MR.strings.background_image_apply), + click = onClick + ) +} + private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { val chatRh = chat.remoteHostId chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { 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 5ba47a390f..5e4146299d 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 @@ -599,13 +599,13 @@ fun ChatLayout( ) { contentPadding -> val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - val defaultBackgroundColor = backgroundImageType?.defaultBackgroundColor - val defaultTintColor = backgroundImageType?.defaultTintColor + val backgroundColor = CurrentColors.value.wallpaper.background ?: backgroundImageType?.defaultBackgroundColor(CurrentColors.value.base) + val tintColor = CurrentColors.value.wallpaper.tint ?: backgroundImageType?.defaultTintColor(CurrentColors.value.base) BoxWithConstraints(Modifier .fillMaxHeight() .background(MaterialTheme.colors.background) - .then(if (backgroundImage != null && backgroundImageType != null && defaultBackgroundColor != null && defaultTintColor != null) - Modifier.drawBehind { chatViewBackground(backgroundImage, backgroundImageType, defaultBackgroundColor, defaultTintColor) } + .then(if (backgroundImage != null && backgroundImageType != null && backgroundColor != null && tintColor != null) + Modifier.drawBehind { chatViewBackground(backgroundImage, backgroundImageType, backgroundColor, tintColor) } else Modifier) .padding(contentPadding) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index c52dd941fd..a6565fde8f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -233,6 +233,16 @@ fun GroupChatInfoLayout( } else { SendReceiptsOptionDisabled() } + // Should come from API + val type = remember { BackgroundImageType.default } + val theme = remember { ThemeOverrides(CurrentColors.value.base, ThemeColors()) } + WallpaperButton { + ModalManager.end.showModal { + WallpaperEditor(type, theme) { type, theme -> + // apply to chat + } + } + } } SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) SectionDividerSpaced(maxTopPadding = true) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index a3b70e65ec..8edc23c81c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -191,6 +191,13 @@ fun FramedItemView( val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Box(Modifier .clip(RoundedCornerShape(18.dp)) + .background( + when { + transparentBackground -> Color.Transparent + sent -> MaterialTheme.colors.background + else -> MaterialTheme.colors.background + } + ) .background( when { transparentBackground -> Color.Transparent 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 4cdcb58d64..c4ca94f45f 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 @@ -11,6 +11,7 @@ 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.common.ui.theme.DefaultTheme import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @@ -20,14 +21,42 @@ import java.io.File import kotlin.math.* @Serializable -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); +enum class PredefinedBackgroundImage( + val res: ImageResource, + val filename: String, + val text: StringResource, + val scale: Float, + val background: Map, + val tint: Map +) { + @SerialName("cat") CAT(MR.images.background_cat, "simplex_cat", MR.strings.background_cat, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ), + @SerialName("hearts") HEARTS(MR.images.background_hearts, "simplex_hearts", MR.strings.background_hearts, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ), + @SerialName("school") SCHOOL(MR.images.background_school, "simplex_school", MR.strings.background_school, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ), + @SerialName("internet") INTERNET(MR.images.background_internet, "simplex_internet", MR.strings.background_internet, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ), + @SerialName("space") SPACE(MR.images.background_space, "simplex_space", MR.strings.background_space, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ), + @SerialName("pets") PETS(MR.images.background_pets, "simplex_pets", MR.strings.background_pets, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ), + @SerialName("rabbit") RABBIT(MR.images.background_rabbit, "simplex_rabbit", MR.strings.background_rabbit, 0.5f, + mapOf(DefaultTheme.LIGHT to Color.White, DefaultTheme.DARK to Color.Black, DefaultTheme.SIMPLEX to Color.Black), + mapOf(DefaultTheme.LIGHT to Color.Blue, DefaultTheme.DARK to Color.Blue, DefaultTheme.SIMPLEX to Color.Blue) + ); fun toType(): BackgroundImageType = BackgroundImageType.Repeated(filename, scale) @@ -48,6 +77,7 @@ enum class BackgroundImageScaleType(val contentScale: ContentScale, val text: St @Serializable sealed class BackgroundImageType { abstract val filename: String + abstract val scale: Float val image by lazy { val cache = cachedImage @@ -66,33 +96,32 @@ sealed class BackgroundImageType { @Serializable @SerialName("repeated") data class Repeated( override val filename: String, - val scale: Float, + override val scale: Float, ): BackgroundImageType() @Serializable @SerialName("static") data class Static( override val filename: String, - val scale: Float, + override val scale: Float, val scaleType: BackgroundImageScaleType, ): BackgroundImageType() - val background: Color? - get() = CurrentColors.value.wallpaper.background - - val tint: Color? - get() = CurrentColors.value.wallpaper.tint - - val defaultBackgroundColor: Color - @Composable get() = if (this is Static) - MaterialTheme.colors.background - else + @Composable + fun defaultBackgroundColor(theme: DefaultTheme): Color = + if (this is Repeated) { + PredefinedBackgroundImage.from(filename)!!.background[theme]!! + } else { MaterialTheme.colors.background + } - val defaultTintColor: Color - @Composable get() = if (this is Repeated || (this is Static && this.scaleType == BackgroundImageScaleType.REPEAT)) + @Composable + fun defaultTintColor(theme: DefaultTheme): Color = + if (this is Repeated) { + PredefinedBackgroundImage.from(filename)!!.tint[theme]!! + } else if (this is Static && scaleType == BackgroundImageScaleType.REPEAT) { MaterialTheme.colors.primary - else + } else { MaterialTheme.colors.background.copy(0.9f) - + } companion object { val default: BackgroundImageType @@ -102,7 +131,7 @@ sealed class BackgroundImageType { } } -fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageType, defaultBackground: Color, defaultTint: Color) = clipRect { +fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageType, background: Color, tint: Color) = clipRect { fun repeat(imageScale: Float) { val scale = imageScale * density for (h in 0..(size.height / image.height / scale).roundToInt()) { @@ -111,22 +140,51 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageT image, dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()), dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()), - colorFilter = ColorFilter.tint(imageType.tint ?: defaultTint) + colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn) ) } } } - 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.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)) - drawRect(imageType.tint ?: defaultTint) + drawRect(background) + when (imageType) { + is BackgroundImageType.Repeated -> repeat(imageType.scale) + is BackgroundImageType.Static -> when (imageType.scaleType) { + BackgroundImageScaleType.REPEAT -> repeat(imageType.scale) + BackgroundImageScaleType.FILL, BackgroundImageScaleType.FIT -> { + 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)) + if (imageType.scaleType == BackgroundImageScaleType.FIT) { + if (scaledWidth < size.width) { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while (x > 0) { + drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight)) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while (x < size.width) { + drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight)) + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while (y > 0) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight)) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while (y < size.height) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight)) + y += scaledHeight + } + } + } + drawRect(tint) + } + } } } 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 2d1b2f9c90..56fb058aeb 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 @@ -14,13 +14,10 @@ import chat.simplex.common.views.chatlist.connectIfOpenedViaUri import chat.simplex.res.MR import com.charleskorn.kaml.decodeFromStream import dev.icerock.moko.resources.StringResource -import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.decodeFromStream import java.io.* import java.net.URI -import java.nio.file.CopyOption import java.nio.file.Files import java.nio.file.StandardCopyOption import java.text.SimpleDateFormat @@ -292,6 +289,7 @@ fun saveBackgroundImage(uri: URI): String? { Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) } catch (e: Exception) { Log.e(TAG, "Error saving background image: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) return null } return destFile.name 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 a5c80d9bee..dfdbbb6a11 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 @@ -1,19 +1,18 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween import SectionSpacer import SectionView -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.* @@ -28,6 +27,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel 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.res.MR import kotlinx.datetime.Clock @@ -35,8 +35,7 @@ import kotlinx.serialization.encodeToString import java.net.URI import java.util.* import kotlin.collections.ArrayList -import kotlin.math.ceil -import kotlin.math.roundToInt +import kotlin.math.* @Composable expect fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) @@ -74,14 +73,21 @@ object AppearanceScope { } @Composable - fun ChatThemePreview(backgroundImage: ImageBitmap?, backgroundImageType: BackgroundImageType?, withMessages: Boolean = true) { + fun ChatThemePreview( + theme: DefaultTheme, + backgroundImage: ImageBitmap?, + backgroundImageType: BackgroundImageType?, + backgroundColor: Color? = CurrentColors.value.wallpaper.background, + tintColor: Color? = CurrentColors.value.wallpaper.tint, + withMessages: Boolean = true + ) { val themeBackgroundColor = MaterialTheme.colors.background - val defaultBackgroundColor = backgroundImageType?.defaultBackgroundColor - val defaultTintColor = backgroundImageType?.defaultTintColor + val backgroundColor = backgroundColor ?: backgroundImageType?.defaultBackgroundColor(theme) + val tintColor = tintColor ?: backgroundImageType?.defaultTintColor(theme) Column(Modifier .drawBehind { - if (backgroundImage != null && backgroundImageType != null && defaultBackgroundColor != null && defaultTintColor != null) { - chatViewBackground(backgroundImage, backgroundImageType, defaultBackgroundColor, defaultTintColor) + if (backgroundImage != null && backgroundImageType != null && backgroundColor != null && tintColor != null) { + chatViewBackground(backgroundImage, backgroundImageType, backgroundColor, tintColor) } else { drawRect(themeBackgroundColor) } @@ -97,157 +103,6 @@ object AppearanceScope { } } - @Composable - fun CustomizeBackgroundImageView() { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { - AppBarTitle(stringResource(MR.strings.choose_background_image_title)) - - val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image - val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - if (backgroundImage != null && backgroundImageType != null) { - ChatThemePreview(backgroundImage, backgroundImageType) - - SectionSpacer() - val isInDarkTheme = isInDarkTheme() - val resetColors = { - ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, null, isInDarkTheme) - ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, null, isInDarkTheme) - } - val imageTypeState = remember { - mutableStateOf(if (backgroundImageType is BackgroundImageType.Static) "" else backgroundImageType.filename) - } - 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 filename = saveBackgroundImage(to) - if (filename != null) { - imageTypeState.value = "" - ThemeManager.saveAndApplyBackgroundImage(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL), systemDark) - removeBackgroundImages(filename) - resetColors() - } - } - } - ExposedDropDownSettingRow( - stringResource(MR.strings.settings_section_title_background_image), - imageTypeValues, - imageTypeState, - onSelected = { filename -> - if (filename.isEmpty()) { - withLongRunningApi { importBackgroundImageLauncher.launch("image/*") } - } else { - imageTypeState.value = filename - ThemeManager.saveAndApplyBackgroundImage(PredefinedBackgroundImage.from(filename)!!.toType(), systemDark) - removeBackgroundImages() - } - } - ) - if (backgroundImageType is BackgroundImageType.Repeated) { - 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) - } - ) - } - } else if (backgroundImageType is BackgroundImageType.Static) { - val state = remember(backgroundImageType.scaleType) { mutableStateOf(backgroundImageType.scaleType) } - val values = remember { - BackgroundImageScaleType.entries.map { it to generalGetString(it.text) } - } - ExposedDropDownSettingRow( - stringResource(MR.strings.background_image_scale), - values, - state, - 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() - var selectedTab by rememberSaveable { mutableStateOf(0) } - val availableTabs = listOf( - stringResource(MR.strings.background_image_background_color), - stringResource(MR.strings.background_image_tint_color), - ) - TabRow( - selectedTabIndex = selectedTab, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - availableTabs.forEachIndexed { index, title -> - Tab( - selected = selectedTab == index, - onClick = { - selectedTab = index - }, - text = { Text(title, fontSize = 13.sp) }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) - } - } - val defaultBackgroundColor = backgroundImageType.defaultBackgroundColor - val defaultTintColor = backgroundImageType.defaultTintColor - if (selectedTab == 0) { - var currentColor by remember(backgroundImageType.background) { mutableStateOf(backgroundImageType.background ?: defaultBackgroundColor) } - ColorPicker(backgroundImageType.background ?: defaultBackgroundColor) { - currentColor = it - ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, it, isInDarkTheme) - } - val clipboard = LocalClipboardManager.current - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { - Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) }) - Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) }) - } - } else { - var currentColor by remember(backgroundImageType.tint) { mutableStateOf(backgroundImageType.tint ?: defaultTintColor) } - ColorPicker(backgroundImageType.tint ?: defaultTintColor) { - currentColor = it - ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, it, isInDarkTheme) - } - val clipboard = LocalClipboardManager.current - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { - Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) }) - Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) }) - } - } - - if (backgroundImageType.background != null || backgroundImageType.tint != null) { - SectionSpacer() - SectionItemView(resetColors) { - Text(generalGetString(MR.strings.reset_color), color = colors.primary) - } - } - SectionBottomSpacer() - } - } - } - @Composable fun ThemesSection( systemDarkTheme: SharedPreference, @@ -260,7 +115,7 @@ object AppearanceScope { val selectedBackground = CurrentColors.collectAsState().value.wallpaper.type val cornerRadius = remember { appPreferences.profileImageCornerRadius.state } fun setBackground(type: BackgroundImageType?) { - if (type is BackgroundImageType.Static) { + if (type is BackgroundImageType.Static || CurrentColors.value.wallpaper.type is BackgroundImageType.Static) { ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, null, systemDark) ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, null, systemDark) } @@ -282,18 +137,21 @@ object AppearanceScope { fun LazyGridScope.gridContent(width: Dp, height: Dp) { @Composable fun BackgroundItem(background: PredefinedBackgroundImage?) { + val checked = (background == null && selectedBackground == null) || (selectedBackground?.filename == background?.filename) Box( Modifier .size(width, height) - .background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt())) + .background(MaterialTheme.colors.background) + .clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt())) + .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius.value.roundToInt())) .clickable { setBackground(background?.toType()) }, contentAlignment = Alignment.Center ) { if (background != null) { val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() } - ChatThemePreview(backgroundImage, background.toType(), withMessages = false) + ChatThemePreview(CurrentColors.value.base, backgroundImage, background.toType(), withMessages = false) } - if ((background == null && selectedBackground == null) || (selectedBackground?.filename == background?.filename)) { + if (checked) { Checked() } } @@ -301,6 +159,8 @@ object AppearanceScope { @Composable fun OwnBackgroundItem(type: BackgroundImageType?) { + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val checked = type is BackgroundImageType.Static && backgroundImage != null val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) { val filename = saveBackgroundImage(to) @@ -313,14 +173,14 @@ object AppearanceScope { Modifier .size(width, height) .background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt())) + .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius.value.roundToInt())) .clickable { withLongRunningApi { importBackgroundImageLauncher.launch("image/*") } }, contentAlignment = Alignment.Center ) { - val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image - if (type is BackgroundImageType.Static && backgroundImage != null) { - ChatThemePreview(backgroundImage, type, withMessages = false) + if (checked) { + ChatThemePreview(CurrentColors.value.base, backgroundImage, type, withMessages = false) Checked() } else { Plus() @@ -362,9 +222,6 @@ object AppearanceScope { } } - SectionItemView(showSettingsModal{ _ -> CustomizeBackgroundImageView() }) { Text(stringResource(MR.strings.choose_background_image_title)) } - - val darkTheme = isSystemInDarkTheme() val state = remember { derivedStateOf { currentTheme.name } } ThemeSelector(state) { @@ -390,25 +247,27 @@ object AppearanceScope { val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - ChatThemePreview(backgroundImage, backgroundImageType) + ChatThemePreview(CurrentColors.value.base, backgroundImage, backgroundImageType) SectionSpacer() if (backgroundImageType != null) { - SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) { - 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.wallpaper.tint ?: backgroundImageType.defaultTintColor - SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT, wallpaperTintColor) }) { - val title = generalGetString(MR.strings.color_wallpaper_tint) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor) + val systemDark = isSystemInDarkTheme() + WallpaperSetupView( + backgroundImageType, + CurrentColors.value.base, + CurrentColors.value.wallpaper.background, + CurrentColors.value.wallpaper.tint, + showPresetSelection = false, + editColor, + onColorChange = { name, color -> + ThemeManager.saveAndApplyThemeColor(name, color, systemDark) + }, + onTypeChange = { type -> + ThemeManager.saveAndApplyBackgroundImage(type, systemDark) + removeBackgroundImages(type?.filename) } - } - SectionSpacer() + ) + SectionDividerSpaced(maxTopPadding = true) } SectionView(stringResource(MR.strings.theme_colors_section_title)) { @@ -503,6 +362,12 @@ object AppearanceScope { fun ColorEditor( name: ThemeColor, initialColor: Color, + theme: DefaultTheme, + backgroundImageType: BackgroundImageType?, + backgroundImage: ImageBitmap?, + previewBackgroundColor: Color? = CurrentColors.value.wallpaper.background, + previewTintColor: Color? = CurrentColors.value.wallpaper.tint, + onColorChange: (Color) -> Unit, close: () -> Unit, ) { Column( @@ -513,9 +378,7 @@ object AppearanceScope { val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) if (supportedLiveChange) { - val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image - val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - ChatThemePreview(backgroundImage, backgroundImageType) + ChatThemePreview(theme, backgroundImage, backgroundImageType, previewBackgroundColor, previewTintColor) SectionSpacer() } @@ -524,28 +387,41 @@ object AppearanceScope { ColorPicker(initialColor) { currentColor = it if (supportedLiveChange) { - ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) + onColorChange(currentColor) } } + val clipboard = LocalClipboardManager.current + Row(Modifier.fillMaxWidth().padding(if (appPlatform.isAndroid) 50.dp else 0.dp), horizontalArrangement = Arrangement.SpaceEvenly) { + Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) }) + Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) }) + } val savedColor = remember { mutableStateOf(initialColor) } DisposableEffect(Unit) { onDispose { if (currentColor != savedColor.value) { // Rollback changes since they weren't saved - ThemeManager.saveAndApplyThemeColor(name, savedColor.value, isInDarkTheme) + onColorChange(savedColor.value) } } } + SectionSpacer() + Row(Modifier.align(Alignment.CenterHorizontally)) { + Box(Modifier.size(80.dp, 40.dp).background(savedColor.value).clickable { + onColorChange(savedColor.value) + currentColor = savedColor.value + }) + Box(Modifier.size(80.dp, 40.dp).background(currentColor)) + } SectionSpacer() TextButton( onClick = { - ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) + onColorChange(currentColor) savedColor.value = currentColor close() }, Modifier.align(Alignment.CenterHorizontally), - colors = ButtonDefaults.textButtonColors(contentColor = currentColor) + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.primary) ) { Text(generalGetString(MR.strings.save_color)) } @@ -594,7 +470,7 @@ object AppearanceScope { @Composable private fun ThemeSelector(state: State, onSelected: (String) -> Unit) { - val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() + val darkTheme = isSystemInDarkTheme() val values by remember(ChatController.appPrefs.appLanguage.state.value) { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) } @@ -630,5 +506,107 @@ object AppearanceScope { //} } +@Composable +fun WallpaperSetupView( + backgroundImageType: BackgroundImageType?, + theme: DefaultTheme, + initialBackgroundColor: Color?, + initialTintColor: Color?, + showPresetSelection: Boolean, + editColor: (ThemeColor, Color) -> Unit, + onColorChange: (ThemeColor, Color?) -> Unit, + onTypeChange: (BackgroundImageType?) -> Unit +) { + SectionView(stringResource(MR.strings.settings_section_title_wallpaper).uppercase()) { + if (showPresetSelection) { + val resetColors = { + onColorChange(ThemeColor.WALLPAPER_BACKGROUND, null) + onColorChange(ThemeColor.WALLPAPER_TINT, null) + } + val imageTypeState: MutableState = remember { + mutableStateOf(if (backgroundImageType is BackgroundImageType.Static) "" else backgroundImageType?.filename) + } + val imageTypeValues = remember { + listOf(null as String? to generalGetString(MR.strings.background_choose_none)) + PredefinedBackgroundImage.entries.map { it.filename to generalGetString(it.text) } + ("" to generalGetString(MR.strings.background_choose_own_image)) + } + val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val filename = saveBackgroundImage(to) + if (filename != null) { + imageTypeState.value = "" + onTypeChange(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL)) + resetColors() + } + } + } + ExposedDropDownSettingRow( + stringResource(MR.strings.settings_section_title_image), + imageTypeValues, + imageTypeState, + onSelected = { filename -> + if (filename == null) { + imageTypeState.value = null + onTypeChange(null) + } + else if (filename.isEmpty()) { + withLongRunningApi { importBackgroundImageLauncher.launch("image/*") } + } else { + imageTypeState.value = filename + onTypeChange(PredefinedBackgroundImage.from(filename)!!.toType()) + } + } + ) + } + + if (backgroundImageType is BackgroundImageType.Static) { + val state = remember(backgroundImageType.scaleType) { mutableStateOf(backgroundImageType.scaleType) } + val values = remember { + BackgroundImageScaleType.entries.map { it to generalGetString(it.text) } + } + ExposedDropDownSettingRow( + stringResource(MR.strings.background_image_scale), + values, + state, + onSelected = { scaleType -> + onTypeChange(backgroundImageType.copy(scaleType = scaleType)) + } + ) + } + + if (backgroundImageType is BackgroundImageType.Repeated || backgroundImageType is BackgroundImageType.Static && backgroundImageType.scaleType == BackgroundImageScaleType.REPEAT) { + val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) } + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp)) + Slider( + state.value, + valueRange = 0.2f..2f, + onValueChange = { + if (backgroundImageType is BackgroundImageType.Repeated) { + onTypeChange(backgroundImageType.copy(scale = it)) + } else if (backgroundImageType is BackgroundImageType.Static) { + onTypeChange(backgroundImageType.copy(scale = it)) + } + } + ) + } + } + + if (backgroundImageType != null) { + val wallpaperBackgroundColor = initialBackgroundColor ?: backgroundImageType.defaultBackgroundColor(theme) + 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 = initialTintColor ?: backgroundImageType.defaultTintColor(theme) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT, wallpaperTintColor) }) { + val title = generalGetString(MR.strings.color_wallpaper_tint) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor) + } + } + } +} + @Composable expect fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) 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 5e4b498288..c7276353b7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -712,7 +712,6 @@ Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]> Appearance Customize theme - Choose background THEME COLORS App version App version: v%s @@ -1035,7 +1034,8 @@ APP ICON THEMES Profile images - Background image + Wallpaper + Image MESSAGES AND FILES CALLS Network connection @@ -1534,10 +1534,11 @@ Title Sent message Received message - Wallpaper background - Wallpaper tint + Background + Tint + None Choose… Cat Hearts @@ -1548,12 +1549,11 @@ Rabbit Hello, Alice Hello, Bob - Background - Tint Scale Repeat Fill Fit + Apply You allow diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index d30e8acca7..6b1441cd56 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -4,8 +4,7 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionView import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -14,8 +13,7 @@ import chat.simplex.common.model.ChatModel import chat.simplex.common.model.SharedPreference import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.defaultLocale -import chat.simplex.common.ui.theme.DEFAULT_PADDING -import chat.simplex.common.ui.theme.ThemeColor +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor import chat.simplex.res.MR @@ -27,13 +25,17 @@ import java.util.Locale @Composable actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { + val darkTheme = isSystemInDarkTheme() + val theme = CurrentColors.collectAsState().value.base + val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image + val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type AppearanceScope.AppearanceLayout( m.controller.appPrefs.appLanguage, m.controller.appPrefs.systemDarkTheme, showSettingsModal = showSettingsModal, editColor = { name, initialColor -> ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, close) + ColorEditor(name, initialColor, theme, backgroundImageType, backgroundImage, onColorChange = { color -> ThemeManager.saveAndApplyThemeColor(name, color, darkTheme) }, close = close) } }, )