diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt index 79dceda1b3..292216ec05 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt @@ -15,7 +15,7 @@ actual val dataDir: File = androidAppContext.dataDir actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE) actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files") actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files") -actual val appearanceDir: File = File(filesDir.absolutePath + File.separator + "appearance") +actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "assets" + File.separator + "wallpapers") actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files" 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 1d656fd105..a21109d705 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 @@ -13,6 +13,7 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -26,9 +27,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import chat.simplex.common.R import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -38,13 +36,9 @@ import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.saveAppLocale import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor import chat.simplex.res.MR -import com.godaddy.android.colorpicker.ClassicColorPicker -import com.godaddy.android.colorpicker.HsvColor import com.smarttoolfactory.colorpicker.model.ColorModel -import com.smarttoolfactory.colorpicker.picker.* import com.smarttoolfactory.colorpicker.selector.SelectorRectHueSaturationHSV import com.smarttoolfactory.colorpicker.slider.SliderCircleColorDisplayValueHSV -import com.smarttoolfactory.colorpicker.widget.ColorDisplayExposedSelectionMenu import com.smarttoolfactory.extendedcolors.util.ColorUtil import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.painterResource @@ -77,9 +71,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 + val baseTheme = CurrentColors.collectAsState().value.base + val backgroundImage = MaterialTheme.wallpaper.type?.image + val backgroundImageType = MaterialTheme.wallpaper.type AppearanceScope.AppearanceLayout( appIcon, m.controller.appPrefs.appLanguage, @@ -88,7 +82,7 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod showSettingsModal = showSettingsModal, editColor = { name, initialColor -> ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, theme, backgroundImageType, backgroundImage, onColorChange = { color -> ThemeManager.saveAndApplyThemeColor(name, color, darkTheme) }, close = close) + ColorEditor(name, initialColor, baseTheme, backgroundImageType, backgroundImage, onColorChange = { color -> ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) }, close = close) } }, ) 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 671bf25c58..9608bfbe7c 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 @@ -1041,7 +1041,8 @@ data class Contact( override val updatedAt: Instant, val chatTs: Instant?, val contactGroupMemberId: Long? = null, - val contactGrpInvSent: Boolean + val contactGrpInvSent: Boolean, + val uiTheme: ThemeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" @@ -1109,7 +1110,8 @@ data class Contact( createdAt = Clock.System.now(), updatedAt = Clock.System.now(), chatTs = Clock.System.now(), - contactGrpInvSent = false + contactGrpInvSent = false, + uiTheme = null, ) } } @@ -1245,7 +1247,8 @@ data class GroupInfo ( val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant, - val chatTs: Instant? + val chatTs: Instant?, + val uiTheme: ThemeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" @@ -1287,7 +1290,8 @@ data class GroupInfo ( chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false), createdAt = Clock.System.now(), updatedAt = Clock.System.now(), - chatTs = Clock.System.now() + chatTs = Clock.System.now(), + uiTheme = null, ) } } 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 73fc9d16a4..556907a42d 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 @@ -1,10 +1,8 @@ package chat.simplex.common.model -import androidx.compose.material.MaterialTheme import chat.simplex.common.views.helpers.* import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg @@ -13,7 +11,6 @@ import chat.simplex.common.model.ChatModel.changingActiveUserMutex import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.views.call.* import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage @@ -438,8 +435,13 @@ object ChatController { Log.d(TAG, "startChatWithTemporaryDatabase") val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl) apiSetNetworkConfig(netCfg, ctrl) - apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) - apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) + apiSetAppFilePaths( + getMigrationTempFilesDirectory().absolutePath, + getMigrationTempFilesDirectory().absolutePath, + wallpapersDir.parentFile.absolutePath, + remoteHostsDir.absolutePath, + ctrl + ) apiStartChat(ctrl) return migrationActiveUser } @@ -658,22 +660,10 @@ object ChatController { } } - suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) { - val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl) + suspend fun apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, remoteHostsFolder: String, ctrl: ChatCtrl? = null) { + val r = sendCmd(null, CC.ApiSetAppFilePaths(filesFolder, tempFolder, assetsFolder, remoteHostsFolder), ctrl) if (r is CR.CmdOk) return - throw Exception("failed to set temp folder: ${r.responseType} ${r.details}") - } - - suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) { - val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl) - if (r is CR.CmdOk) return - throw Exception("failed to set files folder: ${r.responseType} ${r.details}") - } - - suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) { - val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder)) - if (r is CR.CmdOk) return - throw Exception("failed to set remote hosts folder: ${r.responseType} ${r.details}") + throw Exception("failed to set app file paths: ${r.responseType} ${r.details}") } suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable)) @@ -1159,6 +1149,20 @@ object ChatController { return null } + suspend fun apiSetUserUITheme(rh: Long?, userId: Long, theme: ThemeOverrides): Boolean { + val r = sendCmd(rh, CC.ApiSetUserUITheme(userId, theme)) + if (r is CR.CmdOk) return true + Log.e(TAG, "apiSetUserUITheme bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiSetChatUITheme(rh: Long?, chatId: ChatId, theme: ThemeOverrides): Boolean { + val r = sendCmd(rh, CC.ApiSetChatUITheme(chatId, theme)) + if (r is CR.CmdOk) return true + Log.e(TAG, "apiSetChatUITheme bad response: ${r.responseType} ${r.details}") + return false + } + suspend fun apiCreateUserAddress(rh: Long?): String? { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) @@ -2390,9 +2394,8 @@ sealed class CC { class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC() class StartChat(val mainApp: Boolean): CC() class ApiStopChat: CC() - class SetTempFolder(val tempFolder: String): CC() - class SetFilesFolder(val filesFolder: String): CC() - class SetRemoteHostsFolder(val remoteHostsFolder: String): CC() + @Serializable + class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC() class ApiSetEncryptLocalFiles(val enable: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() @@ -2460,6 +2463,8 @@ sealed class CC { class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC() class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() + class ApiSetUserUITheme(val userId: Long, val theme: ThemeOverrides): CC() + class ApiSetChatUITheme(val chatId: String, val theme: ThemeOverrides): CC() class ApiCreateMyAddress(val userId: Long): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() @@ -2527,9 +2532,7 @@ sealed class CC { is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" is StartChat -> "/_start main=${onOff(mainApp)}" is ApiStopChat -> "/_stop" - is SetTempFolder -> "/_temp_folder $tempFolder" - is SetFilesFolder -> "/_files_folder $filesFolder" - is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder" + is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}" is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" @@ -2606,6 +2609,8 @@ sealed class CC { is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" + is ApiSetUserUITheme -> "/_set theme user $userId ${json.encodeToString(theme)}" + is ApiSetChatUITheme -> "/_set theme $chatId ${json.encodeToString(theme)}" is ApiCreateMyAddress -> "/_address $userId" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" @@ -2669,9 +2674,7 @@ sealed class CC { is ApiDeleteUser -> "apiDeleteUser" is StartChat -> "startChat" is ApiStopChat -> "apiStopChat" - is SetTempFolder -> "setTempFolder" - is SetFilesFolder -> "setFilesFolder" - is SetRemoteHostsFolder -> "setRemoteHostsFolder" + is ApiSetAppFilePaths -> "apiSetAppFilePaths" is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" @@ -2739,6 +2742,8 @@ sealed class CC { is ApiSetContactPrefs -> "apiSetContactPrefs" is ApiSetContactAlias -> "apiSetContactAlias" is ApiSetConnectionAlias -> "apiSetConnectionAlias" + is ApiSetUserUITheme -> "apiSetUserUITheme" + is ApiSetChatUITheme -> "apiSetChatUITheme" is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiShowMyAddress -> "apiShowMyAddress" @@ -5428,6 +5433,8 @@ data class AppSettings( var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null, var iosCallKitEnabled: Boolean? = null, var iosCallKitCallsInRecents: Boolean? = null, + var uiColorScheme: String? = null, + var uiThemes: List? = null, ) { fun prepareForExport(): AppSettings { val empty = AppSettings() @@ -5451,6 +5458,8 @@ data class AppSettings( if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen } if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled } if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme } + if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes } return empty } @@ -5482,6 +5491,8 @@ data class AppSettings( androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) } iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) } iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) } + uiColorScheme?.let { def.currentTheme.set(it) } + uiThemes?.let { def.themeOverrides.set(it.map { theme -> theme.base.name to theme }.toMap()) } } companion object { @@ -5505,7 +5516,9 @@ data class AppSettings( confirmDBUpgrades = false, androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW, iosCallKitEnabled = true, - iosCallKitCallsInRecents = false + iosCallKitCallsInRecents = false, + uiColorScheme = DefaultTheme.SYSTEM.name, + uiThemes = null, ) val current: AppSettings @@ -5531,6 +5544,8 @@ data class AppSettings( androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()), iosCallKitEnabled = def.iosCallKitEnabled.get(), iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(), + uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM.name, + uiThemes = def.themeOverrides.get().values.toList(), ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 0d447a4a5a..efc817371e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -88,11 +88,13 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat return } platform.androidRestartNetworkObserver() - controller.apiSetTempFolder(coreTmpDir.absolutePath) - controller.apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } + controller.apiSetAppFilePaths( + appFilesDir.absolutePath, + coreTmpDir.absolutePath, + wallpapersDir.parentFile.absolutePath, + remoteHostsDir.absolutePath, + ctrl + ) controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get()) // If we migrated successfully means previous re-encryption process on database level finished successfully too if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 09dee94576..5e9299b302 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -14,7 +14,7 @@ expect val dataDir: File expect val tmpDir: File expect val filesDir: File expect val appFilesDir: File -expect val appearanceDir: File +expect val wallpapersDir: File expect val coreTmpDir: File expect val dbAbsolutePrefixPath: String @@ -83,7 +83,7 @@ fun getBackgroundImageFilePath(fileName: String): String { val rh = chatModel.currentRemoteHost.value val s = File.separator val path = if (rh == null) { - appearanceDir.absolutePath + s + fileName + wallpapersDir.absolutePath + s + fileName } else { remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index 5eeedbb2a0..b465e9e174 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt @@ -34,7 +34,7 @@ val FileDark = Color(101, 101, 106, 255) val SentMessageColor = Color(0x1E45B8FF) val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black -val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) { +val NoteFolderIconColor: Color @Composable get() = with(MaterialTheme.appColors.sentMessage) { // Default color looks too light and better to have it here a little bit brighter if (alpha == SentMessageColor.alpha) { copy(min(SentMessageColor.alpha + 0.1f, 1f)) 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 40b198713b..238c91cafc 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 @@ -46,17 +46,73 @@ enum class DefaultTheme { } } -data class AppColors( - val title: Color, - val sentMessage: Color, - val receivedMessage: Color, -) +@Stable +class AppColors( + title: Color, + sentMessage: Color, + receivedMessage: Color +) { + var title by mutableStateOf(title, structuralEqualityPolicy()) + internal set + var sentMessage by mutableStateOf(sentMessage, structuralEqualityPolicy()) + internal set + var receivedMessage by mutableStateOf(receivedMessage, structuralEqualityPolicy()) + internal set -data class AppWallpaper( - val background: Color? = null, - val tint: Color? = null, - val type: BackgroundImageType? = null -) + fun copy( + title: Color = this.title, + sentMessage: Color = this.sentMessage, + receivedMessage: Color = this.receivedMessage, + ): AppColors = AppColors( + title, + sentMessage, + receivedMessage, + ) + + override fun toString(): String { + return buildString { + append("AppColors(") + append("title=$title, ") + append("sentMessage=$sentMessage, ") + append("receivedMessage=$receivedMessage") + append(")") + } + } +} + +@Stable +class AppWallpaper( + background: Color? = null, + tint: Color? = null, + type: BackgroundImageType? = null, +) { + var background by mutableStateOf(background, structuralEqualityPolicy()) + internal set + var tint by mutableStateOf(tint, structuralEqualityPolicy()) + internal set + var type by mutableStateOf(type, structuralEqualityPolicy()) + internal set + + fun copy( + background: Color? = this.background, + tint: Color? = this.tint, + type: BackgroundImageType? = this.type, + ): AppWallpaper = AppWallpaper( + background, + tint, + type, + ) + + override fun toString(): String { + return buildString { + append("AppWallpaper(") + append("background=$background, ") + append("tint=$tint, ") + append("type=$type") + append(")") + } + } +} enum class ThemeColor { PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE, WALLPAPER_BACKGROUND, WALLPAPER_TINT; @@ -166,6 +222,7 @@ data class ThemeWallpaper ( val background: String? = null, val tint: String? = null, val image: String? = null, + val imageFile: String? = null, ) { fun toAppWallpaper(): AppWallpaper { return AppWallpaper( @@ -173,9 +230,9 @@ data class ThemeWallpaper ( tint = tint?.colorFromReadableHex(), type = if (preset != null) { BackgroundImageType.Repeated(filename = preset, scale) - } else if (image != null) { + } else if (imageFile != null) { BackgroundImageType.Static( - filename = image, + filename = imageFile, scale, scaleType ?: BackgroundImageScaleType.FILL ) @@ -184,13 +241,29 @@ data class ThemeWallpaper ( ) } - fun withFilledWallpaper(): ThemeWallpaper { + fun withFilledWallpaperBase64(): ThemeWallpaper { val aw = toAppWallpaper() + val type = aw.type 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, + image = if (type is BackgroundImageType.Static && type.image != null) resizeImageToStrSize(type.image!!, 5_000_000) else null, + imageFile = null, + preset = if (type is BackgroundImageType.Repeated) type.filename else null, + scale = if (type is BackgroundImageType.Repeated) type.scale else if (type != null) (type as BackgroundImageType.Static).scale else 1f, + scaleType = if (type is BackgroundImageType.Static) type.scaleType else null, + background = aw.background?.toReadableHex(), + tint = aw.tint?.toReadableHex(), + ) + } + + fun withFilledWallpaperPath(): ThemeWallpaper { + val aw = toAppWallpaper() + val type = aw.type + return ThemeWallpaper( + image = null, + imageFile = 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 if (type != null) (type as BackgroundImageType.Static).scale else 1f, + scaleType = if (type is BackgroundImageType.Static) type.scaleType else null, background = aw.background?.toReadableHex(), tint = aw.tint?.toReadableHex(), ) @@ -211,7 +284,8 @@ data class ThemeWallpaper ( companion object { fun from(type: BackgroundImageType, background: String?, tint: String?): ThemeWallpaper { return ThemeWallpaper( - image = if (type is BackgroundImageType.Static) type.filename else null, + image = null, + imageFile = 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, @@ -224,8 +298,8 @@ data class ThemeWallpaper ( @Serializable data class ThemeOverrides ( - val base: DefaultTheme, - val colors: ThemeColors, + val base: DefaultTheme = CurrentColors.value.base, + val colors: ThemeColors = ThemeColors(), val wallpaper: ThemeWallpaper = ThemeWallpaper(), ) { fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides { @@ -343,6 +417,25 @@ fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLi @Composable expect fun isSystemInDarkTheme(): Boolean +internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp } +internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() } + +val MaterialTheme.appColors: AppColors + @Composable + @ReadOnlyComposable + get() = LocalAppColors.current + +fun AppColors.updateColorsFrom(other: AppColors) { + title = other.title + sentMessage = other.sentMessage + receivedMessage = other.receivedMessage +} + +val MaterialTheme.wallpaper: AppWallpaper + @Composable + @ReadOnlyComposable + get() = LocalAppWallpaper.current + fun reactOnDarkThemeChanges(isDark: Boolean) { if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) { // Change active colors from light to dark and back based on system theme @@ -367,7 +460,32 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { typography = Typography, shapes = Shapes, content = { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content) + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + LocalAppColors provides theme.appColors, + LocalAppWallpaper provides theme.wallpaper, + content = content) + } + ) +} + +@Composable +fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Unit) { + MaterialTheme( + colors = theme.colors, + typography = Typography, + shapes = Shapes, + content = { + val rememberedAppColors = remember { + // Explicitly creating a new object here so we don't mutate the initial [appColors] + // provided, and overwrite the values set in it. + theme.appColors.copy() + }.apply { updateColorsFrom(theme.appColors) } + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + LocalAppColors provides rememberedAppColors, + LocalAppWallpaper provides theme.wallpaper, + content = content) } ) } 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 d30be5c6f1..95b8132bac 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 @@ -25,10 +25,8 @@ object ThemeManager { else -> SimplexColorPalette to DefaultTheme.SIMPLEX } - fun currentColors(darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides): ActiveTheme { + fun currentColors(darkForSystemTheme: Boolean, themeOverrides: Map = appPrefs.themeOverrides.get()): ActiveTheme { val themeName = appPrefs.currentTheme.get()!! - val themeOverrides = pref.get() - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { themeName } else { @@ -48,6 +46,17 @@ object ThemeManager { return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base, backgroundTheme), theme.colors.toAppColors(theme.base, backgroundTheme), theme.wallpaper.toAppWallpaper()) } + fun overriddenColors(theme: ThemeOverrides): ActiveTheme { + val baseTheme = when (theme.base.name) { + DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) + DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) + else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + } + val backgroundTheme = if (theme.wallpaper.preset != null) PredefinedBackgroundImage.from(theme.wallpaper.preset)?.colors?.get(theme.base) else null + return ActiveTheme(theme.base.name, baseTheme.first, theme.colors.toColors(theme.base, backgroundTheme), theme.colors.toAppColors(theme.base, backgroundTheme), theme.wallpaper.toAppWallpaper()) + } + fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { @@ -56,9 +65,9 @@ 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(), wallpaper = ThemeWallpaper()) + val nonFilledTheme = overrides.getOrDefault(nonSystemThemeName, ThemeOverrides()) val backgroundTheme = if (nonFilledTheme.wallpaper.preset != null) PredefinedBackgroundImage.from(nonFilledTheme.wallpaper.preset)?.colors?.get(nonFilledTheme.base) else null - return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base, backgroundTheme), wallpaper = nonFilledTheme.wallpaper.withFilledWallpaper()) + return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base, backgroundTheme), wallpaper = nonFilledTheme.wallpaper.withFilledWallpaperBase64()) } // colors, default theme enum, localized name of theme @@ -97,22 +106,17 @@ object ThemeManager { fun applyTheme(theme: String, darkForSystemTheme: Boolean) { appPrefs.currentTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides) + CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() } fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { appPrefs.systemDarkTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides) + CurrentColors.value = currentColors(darkForSystemTheme, appPrefs.themeOverrides.get()) } - 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 - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name - } + fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference> = appPrefs.themeOverrides) { + val nonSystemThemeName = baseTheme.name var colorToSet = color if (colorToSet == null) { // Setting default color from a base theme @@ -125,33 +129,30 @@ object ThemeManager { } } val overrides = pref.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) + val prevValue = overrides.getOrDefault(nonSystemThemeName, ThemeOverrides()) overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet?.toReadableHex()) pref.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) } - 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 - } + fun saveAndApplyBackgroundImage(baseTheme: DefaultTheme, type: BackgroundImageType?, pref: SharedPreference> = appPrefs.themeOverrides) { + val nonSystemThemeName = baseTheme.name val overrides = pref.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) + // val prevValue = overrides.getOrDefault(nonSystemThemeName, ThemeOverrides()) + // Overriding the whole theme on type change + val prevValue = ThemeOverrides() overrides[nonSystemThemeName] = prevValue.copy(wallpaper = if (type != null) ThemeWallpaper.from(type, prevValue.wallpaper.background, prevValue.wallpaper.tint) else ThemeWallpaper()) pref.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) } - fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { + fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference> = appPrefs.themeOverrides) { val overrides = pref.get().toMutableMap() - val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper()) + val prevValue = overrides.getOrDefault(theme.base.name, ThemeOverrides()) 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, appPrefs.themeOverrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) } fun resetAllThemeColors(darkForSystemTheme: Boolean, pref: SharedPreference> = appPrefs.themeOverrides) { @@ -165,11 +166,11 @@ object ThemeManager { val prevValue = overrides[nonSystemThemeName] ?: return overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper.copy(background = null, tint = null)) pref.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight, appPrefs.themeOverrides.get()) } fun String.colorFromReadableHex(): Color = Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) - fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) + fun Color.toReadableHex(): String = "#" + if (this == Color.Transparent) "00ffffff" else Integer.toHexString(toArgb()) } 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 dfa713f414..7bc8c4c154 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,7 +29,6 @@ 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.* @@ -339,13 +338,18 @@ 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()) } + + val theme = remember { (chat.chatInfo as ChatInfo.Direct).contact.uiTheme ?: ThemeOverrides() } WallpaperButton { ModalManager.end.showModal { - WallpaperEditor(type, theme) { type, theme -> - // apply to chat + WallpaperEditor(theme) { type, theme -> + withBGApi { + if (controller.apiSetChatUITheme(chat.remoteHostId, chat.id, theme.copy(wallpaper = theme.wallpaper.withFilledWallpaperPath()))) { + // Remove previous image only after saving + removeBackgroundImage(theme.wallpaper.imageFile) + chatModel.updateChatInfo(chat.remoteHostId, (chat.chatInfo as ChatInfo.Direct).copy(contact = chat.chatInfo.contact.copy(uiTheme = theme.copy(wallpaper = theme.wallpaper.withFilledWallpaperPath())))) + } + } } } } @@ -697,36 +701,32 @@ fun ShareAddressButton(onClick: () -> Unit) { } @Composable -fun ModalData.WallpaperEditor(type: BackgroundImageType, theme: ThemeOverrides, save: (BackgroundImageType?, ThemeOverrides) -> Unit) { +fun ModalData.WallpaperEditor(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 baseTheme = CurrentColors.value.base val themeOverrides = remember { stateGetOrPut("themeOverrides") { theme } } + val backgroundImageType: State = remember { derivedStateOf { BackgroundImageType.from(themeOverrides.value.wallpaper) } } val pref = remember { SharedPreference>( get = { - mapOf(CurrentColors.value.base.name to themeOverrides.value) + mapOf(baseTheme.name to themeOverrides.value) }, set = { value -> - themeOverrides.value = value[CurrentColors.value.base.name]!! + themeOverrides.value = value[baseTheme.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() + val backgroundImage = remember { derivedStateOf { backgroundImageType.value?.image } } + val backgroundColor = remember { derivedStateOf { themeOverrides.value.wallpaper.background?.colorFromReadableHex() } } + val tintColor = remember { derivedStateOf { themeOverrides.value.wallpaper.tint?.colorFromReadableHex() } } WallpaperSetupView( - background, + backgroundImageType.value, theme.base, backgroundColor.value, tintColor.value, @@ -738,26 +738,21 @@ fun ModalData.WallpaperEditor(type: BackgroundImageType, theme: ThemeOverrides, initialColor, theme.base, backgroundImageType.value, - backgroundImage, + backgroundImage.value, 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 + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color, pref) }, 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 + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color, pref) }, onTypeChange = { type -> - ThemeManager.saveAndApplyBackgroundImage(type, systemDark, pref) - backgroundImageType.value = type + ThemeManager.saveAndApplyBackgroundImage(baseTheme, type, pref) } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index df8e535f82..2dab2fe7c8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -42,7 +42,7 @@ sealed class CIInfoTab { @Composable fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { val sent = ci.chatDir.sent - val appColors = CurrentColors.collectAsState().value.appColors + val appColors = MaterialTheme.appColors val uriHandler = LocalUriHandler.current val selection = remember { mutableStateOf(CIInfoTab.History) } 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 5e4146299d..75db512dcd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -117,368 +117,372 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: val clipboard = LocalClipboardManager.current when (chat.chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { - ChatLayout( - chat, - unreadCount, - composeState, - composeView = { - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if ( - chat.chatInfo is ChatInfo.Direct - && !chat.chatInfo.contact.ready - && chat.chatInfo.contact.active - && !chat.chatInfo.contact.nextSendGrpInv + val themeOverrides = remember(chat.chatInfo) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiTheme else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiTheme else null } + val overrides = if (themeOverrides != null) ThemeManager.overriddenColors(themeOverrides) else null + SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { + ChatLayout( + chat, + unreadCount, + composeState, + composeView = { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - generalGetString(MR.strings.contact_connection_pending), - Modifier.padding(top = 4.dp), - fontSize = 14.sp, - color = MaterialTheme.colors.secondary + if ( + chat.chatInfo is ChatInfo.Direct + && !chat.chatInfo.contact.ready + && chat.chatInfo.contact.active + && !chat.chatInfo.contact.nextSendGrpInv + ) { + Text( + generalGetString(MR.strings.contact_connection_pending), + Modifier.padding(top = 4.dp), + fontSize = 14.sp, + color = MaterialTheme.colors.secondary + ) + } + ComposeView( + chatModel, chat, composeState, attachmentOption, + showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } ) } - ComposeView( - chatModel, chat, composeState, attachmentOption, - showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } - ) - } - }, - attachmentOption, - attachmentBottomSheetState, - searchText, - useLinkPreviews = useLinkPreviews, - linkMode = chatModel.simplexLinkMode.value, - back = { - hideKeyboard(view) - AudioPlayer.stop() - chatModel.chatId.value = null - chatModel.groupMembers.clear() - }, - info = { - if (ModalManager.end.hasModalsOpen()) { - ModalManager.end.closeModals() - return@ChatLayout - } - hideKeyboard(view) - withBGApi { - // The idea is to preload information before showing a modal because large groups can take time to load all members - var preloadedContactInfo: Pair? = null - var preloadedCode: String? = null - var preloadedLink: Pair? = null - if (chat.chatInfo is ChatInfo.Direct) { - preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - } else if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) + }, + attachmentOption, + attachmentBottomSheetState, + searchText, + useLinkPreviews = useLinkPreviews, + linkMode = chatModel.simplexLinkMode.value, + back = { + hideKeyboard(view) + AudioPlayer.stop() + chatModel.chatId.value = null + chatModel.groupMembers.clear() + }, + info = { + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout } - ModalManager.end.showModalCloseable(true) { close -> - val chat = remember { activeChat }.value - if (chat?.chatInfo is ChatInfo.Direct) { - var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } - var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { - contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedContactInfo = contactInfo - code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - preloadedCode = code - } - ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) - } else if (chat?.chatInfo is ChatInfo.Group) { - var link: Pair? by remember(chat.id) { mutableStateOf(preloadedLink) } - KeyChangeEffect(chat.id) { - setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) - link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) - preloadedLink = link - } - GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, { - link = it - preloadedLink = it - }, close) - } - } - } - }, - showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> - hideKeyboard(view) - withBGApi { - val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) - val stats = r?.second - val (_, code) = if (member.memberActive) { - val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId) - member to memCode?.second - } else { - member to null - } - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) - } - } - } - }, - loadPrevMessages = { - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) - val firstId = chatModel.chatItems.value.firstOrNull()?.id - if (c != null && firstId != null) { + hideKeyboard(view) withBGApi { - apiLoadPrevMessages(c, chatModel, firstId, searchText.value) + // The idea is to preload information before showing a modal because large groups can take time to load all members + var preloadedContactInfo: Pair? = null + var preloadedCode: String? = null + var preloadedLink: Pair? = null + if (chat.chatInfo is ChatInfo.Direct) { + preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) + preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second + } else if (chat.chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) + preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) + } + ModalManager.end.showModalCloseable(true) { close -> + val chat = remember { activeChat }.value + if (chat?.chatInfo is ChatInfo.Direct) { + var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) + preloadedContactInfo = contactInfo + code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second + preloadedCode = code + } + ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) + } else if (chat?.chatInfo is ChatInfo.Group) { + var link: Pair? by remember(chat.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chat.id) { + setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) + preloadedLink = link + } + GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, { + link = it + preloadedLink = it + }, close) + } + } } - } - }, - deleteMessage = { itemId, mode -> - withBGApi { - val cInfo = chat.chatInfo - val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } - val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) - val groupInfo = toModerate?.first - val groupMember = toModerate?.second - val deletedChatItem: ChatItem? - val toChatItem: ChatItem? - if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { - val r = chatModel.controller.apiDeleteMemberChatItem( - chatRh, - groupId = groupInfo.groupId, - groupMemberId = groupMember.groupMemberId, - itemId = itemId - ) - deletedChatItem = r?.first - toChatItem = r?.second - } else { - val r = chatModel.controller.apiDeleteChatItem( - chatRh, + }, + showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> + hideKeyboard(view) + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) + val stats = r?.second + val (_, code) = if (member.memberActive) { + val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId) + member to memCode?.second + } else { + member to null + } + setGroupMembers(chatRh, groupInfo, chatModel) + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(true) { close -> + remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> + GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) + } + } + } + }, + loadPrevMessages = { + if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout + val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) + val firstId = chatModel.chatItems.value.firstOrNull()?.id + if (c != null && firstId != null) { + withBGApi { + apiLoadPrevMessages(c, chatModel, firstId, searchText.value) + } + } + }, + deleteMessage = { itemId, mode -> + withBGApi { + val cInfo = chat.chatInfo + val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } + val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) + val groupInfo = toModerate?.first + val groupMember = toModerate?.second + val deletedChatItem: ChatItem? + val toChatItem: ChatItem? + if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { + val r = chatModel.controller.apiDeleteMemberChatItem( + chatRh, + groupId = groupInfo.groupId, + groupMemberId = groupMember.groupMemberId, + itemId = itemId + ) + deletedChatItem = r?.first + toChatItem = r?.second + } else { + val r = chatModel.controller.apiDeleteChatItem( + chatRh, + type = cInfo.chatType, + id = cInfo.apiId, + itemId = itemId, + mode = mode + ) + deletedChatItem = r?.deletedChatItem?.chatItem + toChatItem = r?.toChatItem?.chatItem + } + if (toChatItem == null && deletedChatItem != null) { + chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) + } else if (toChatItem != null) { + chatModel.upsertChatItem(chatRh, cInfo, toChatItem) + } + } + }, + deleteMessages = { itemIds -> + if (itemIds.isNotEmpty()) { + val chatInfo = chat.chatInfo + withBGApi { + val deletedItems: ArrayList = arrayListOf() + for (itemId in itemIds) { + val di = chatModel.controller.apiDeleteChatItem( + chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal + )?.deletedChatItem?.chatItem + if (di != null) { + deletedItems.add(di) + } + } + for (di in deletedItems) { + chatModel.removeChatItem(chatRh, chatInfo, di) + } + } + } + }, + receiveFile = { fileId -> + withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } + }, + cancelFile = { fileId -> + withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } + }, + joinGroup = { groupId, onComplete -> + withBGApi { + chatModel.controller.apiJoinGroup(chatRh, groupId) + onComplete.invoke() + } + }, + startCall = out@{ media -> + withBGApi { + val cInfo = chat.chatInfo + if (cInfo is ChatInfo.Direct) { + val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) + val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi + chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) + chatModel.showCallView.value = true + chatModel.callCommand.add(WCallCommand.Capabilities(media)) + } + } + }, + endCall = { + val call = chatModel.activeCall.value + if (call != null) withBGApi { chatModel.callManager.endCall(call) } + }, + acceptCall = { contact -> + hideKeyboard(view) + withBGApi { + val invitation = chatModel.callInvitations.remove(contact.id) + ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } + if (invitation == null) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) + } else { + chatModel.callManager.acceptIncomingCall(invitation = invitation) + } + } + }, + acceptFeature = { contact, feature, param -> + withBGApi { + chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) + } + }, + openDirectChat = { contactId -> + withBGApi { + openDirectChat(chatRh, contactId, chatModel) + } + }, + updateContactStats = { contact -> + withBGApi { + val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) + if (r != null) { + val contactStats = r.first + if (contactStats != null) + chatModel.updateContactConnectionStats(chatRh, contact, contactStats) + } + } + }, + updateMemberStats = { groupInfo, member -> + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) + if (r != null) { + val memStats = r.second + if (memStats != null) { + chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + } + } + } + }, + syncContactConnection = { contact -> + withBGApi { + val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) + if (cStats != null) { + chatModel.updateContactConnectionStats(chatRh, contact, cStats) + } + } + }, + syncMemberConnection = { groupInfo, member -> + withBGApi { + val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) + if (r != null) { + chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + } + } + }, + findModelChat = { chatId -> + chatModel.getChat(chatId) + }, + findModelMember = { memberId -> + chatModel.groupMembers.find { it.id == memberId } + }, + setReaction = { cInfo, cItem, add, reaction -> + withBGApi { + val updatedCI = chatModel.controller.apiChatItemReaction( + rh = chatRh, type = cInfo.chatType, id = cInfo.apiId, - itemId = itemId, - mode = mode + itemId = cItem.id, + add = add, + reaction = reaction ) - deletedChatItem = r?.deletedChatItem?.chatItem - toChatItem = r?.toChatItem?.chatItem + if (updatedCI != null) { + chatModel.updateChatItem(cInfo, updatedCI) + } } - if (toChatItem == null && deletedChatItem != null) { - chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) - } else if (toChatItem != null) { - chatModel.upsertChatItem(chatRh, cInfo, toChatItem) - } - } - }, - deleteMessages = { itemIds -> - if (itemIds.isNotEmpty()) { - val chatInfo = chat.chatInfo - withBGApi { - val deletedItems: ArrayList = arrayListOf() - for (itemId in itemIds) { - val di = chatModel.controller.apiDeleteChatItem( - chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal - )?.deletedChatItem?.chatItem - if (di != null) { - deletedItems.add(di) + }, + showItemDetails = { cInfo, cItem -> + suspend fun loadChatItemInfo(): ChatItemInfo? { + val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) + if (ciInfo != null) { + if (chat.chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) } } - for (di in deletedItems) { - chatModel.removeChatItem(chatRh, chatInfo, di) - } + return ciInfo } - } - }, - receiveFile = { fileId -> - withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } - }, - cancelFile = { fileId -> - withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } - }, - joinGroup = { groupId, onComplete -> - withBGApi { - chatModel.controller.apiJoinGroup(chatRh, groupId) - onComplete.invoke() - } - }, - startCall = out@{ media -> - withBGApi { - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) - val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) - chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(media)) - } - } - }, - endCall = { - val call = chatModel.activeCall.value - if (call != null) withBGApi { chatModel.callManager.endCall(call) } - }, - acceptCall = { contact -> - hideKeyboard(view) - withBGApi { - val invitation = chatModel.callInvitations.remove(contact.id) - ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } - if (invitation == null) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) - } else { - chatModel.callManager.acceptIncomingCall(invitation = invitation) - } - } - }, - acceptFeature = { contact, feature, param -> - withBGApi { - chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) - } - }, - openDirectChat = { contactId -> - withBGApi { - openDirectChat(chatRh, contactId, chatModel) - } - }, - updateContactStats = { contact -> - withBGApi { - val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - if (r != null) { - val contactStats = r.first - if (contactStats != null) - chatModel.updateContactConnectionStats(chatRh, contact, contactStats) - } - } - }, - updateMemberStats = { groupInfo, member -> - withBGApi { - val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) - if (r != null) { - val memStats = r.second - if (memStats != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) - } - } - } - }, - syncContactConnection = { contact -> - withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } - } - }, - syncMemberConnection = { groupInfo, member -> - withBGApi { - val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) - if (r != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) - } - } - }, - findModelChat = { chatId -> - chatModel.getChat(chatId) - }, - findModelMember = { memberId -> - chatModel.groupMembers.find { it.id == memberId } - }, - setReaction = { cInfo, cItem, add, reaction -> - withBGApi { - val updatedCI = chatModel.controller.apiChatItemReaction( - rh = chatRh, - type = cInfo.chatType, - id = cInfo.apiId, - itemId = cItem.id, - add = add, - reaction = reaction - ) - if (updatedCI != null) { - chatModel.updateChatItem(cInfo, updatedCI) - } - } - }, - showItemDetails = { cInfo, cItem -> - suspend fun loadChatItemInfo(): ChatItemInfo? { - val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) - if (ciInfo != null) { - if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - } - } - return ciInfo - } - withBGApi { - var initialCiInfo = loadChatItemInfo() ?: return@withBGApi - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(endButtons = { - ShareButton { - clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get())) - } - }) { close -> - var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) } - ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) - LaunchedEffect(cItem.id) { - withContext(Dispatchers.Default) { - for (apiResp in controller.messagesChannel) { - val msg = apiResp.resp - if (apiResp.remoteHostId == chatRh && - msg is CR.ChatItemStatusUpdated && - msg.chatItem.chatItem.id == cItem.id - ) { - ciInfo = loadChatItemInfo() ?: return@withContext - initialCiInfo = ciInfo + withBGApi { + var initialCiInfo = loadChatItemInfo() ?: return@withBGApi + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(endButtons = { + ShareButton { + clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get())) + } + }) { close -> + var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) } + ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + LaunchedEffect(cItem.id) { + withContext(Dispatchers.Default) { + for (apiResp in controller.messagesChannel) { + val msg = apiResp.resp + if (apiResp.remoteHostId == chatRh && + msg is CR.ChatItemStatusUpdated && + msg.chatItem.chatItem.id == cItem.id + ) { + ciInfo = loadChatItemInfo() ?: return@withContext + initialCiInfo = ciInfo + } } } } - } - KeyChangeEffect(chatModel.chatId.value) { - close() + KeyChangeEffect(chatModel.chatId.value) { + close() + } } } - } - }, - addMembers = { groupInfo -> - hideKeyboard(view) - withBGApi { - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) + }, + addMembers = { groupInfo -> + hideKeyboard(view) + withBGApi { + setGroupMembers(chatRh, groupInfo, chatModel) + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(true) { close -> + AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) + } } - } - }, - openGroupLink = { groupInfo -> - hideKeyboard(view) - withBGApi { - val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + }, + openGroupLink = { groupInfo -> + hideKeyboard(view) + withBGApi { + val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(true) { + GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + } } - } - }, - markRead = { range, unreadCountAfter -> - chatModel.markChatItemsRead(chat, range, unreadCountAfter) - ntfManager.cancelNotificationsForChat(chat.id) - withBGApi { - chatModel.controller.apiChatRead( - chatRh, - chat.chatInfo.chatType, - chat.chatInfo.apiId, - range - ) - } - }, - changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, - onSearchValueChanged = { value -> - if (searchText.value == value) return@ChatLayout - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout - withBGApi { - apiFindMessages(c, chatModel, value) - searchText.value = value - } - }, - onComposed, - developerTools = chatModel.controller.appPrefs.developerTools.get(), - ) + }, + markRead = { range, unreadCountAfter -> + chatModel.markChatItemsRead(chat, range, unreadCountAfter) + ntfManager.cancelNotificationsForChat(chat.id) + withBGApi { + chatModel.controller.apiChatRead( + chatRh, + chat.chatInfo.chatType, + chat.chatInfo.apiId, + range + ) + } + }, + changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, + onSearchValueChanged = { value -> + if (searchText.value == value) return@ChatLayout + if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout + val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout + withBGApi { + apiFindMessages(c, chatModel, value) + searchText.value = value + } + }, + onComposed, + developerTools = chatModel.controller.appPrefs.developerTools.get(), + ) + } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } @@ -597,10 +601,10 @@ fun ChatLayout( drawerContentColor = LocalContentColor.current, backgroundColor = Color.Unspecified ) { contentPadding -> - val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image - val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - val backgroundColor = CurrentColors.value.wallpaper.background ?: backgroundImageType?.defaultBackgroundColor(CurrentColors.value.base) - val tintColor = CurrentColors.value.wallpaper.tint ?: backgroundImageType?.defaultTintColor(CurrentColors.value.base) + val backgroundImage = MaterialTheme.wallpaper.type?.image + val backgroundImageType = MaterialTheme.wallpaper.type + val backgroundColor = MaterialTheme.wallpaper.background ?: backgroundImageType?.defaultBackgroundColor(CurrentColors.value.base) + val tintColor = MaterialTheme.wallpaper.tint ?: backgroundImageType?.defaultTintColor(CurrentColors.value.base) BoxWithConstraints(Modifier .fillMaxHeight() .background(MaterialTheme.colors.background) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt index 20316dd524..bc82bc593f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt @@ -16,7 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource @Composable fun ComposeContextInvitingContactMemberView() { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .height(60.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt index 83076f885b..7ab7963547 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt @@ -16,7 +16,7 @@ import chat.simplex.res.MR @Composable fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .height(60.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt index 906065f741..97b6f9afda 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt @@ -20,7 +20,7 @@ import chat.simplex.common.views.helpers.UploadContent @Composable fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .padding(top = 8.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index d4e9f0dea5..7ea5fd3d06 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -775,7 +775,7 @@ fun ComposeView( @Composable fun MsgNotAllowedView(reason: String, icon: Painter) { - val color = CurrentColors.collectAsState().value.appColors.receivedMessage + val color = MaterialTheme.appColors.receivedMessage Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, tint = MaterialTheme.colors.secondary) Spacer(Modifier.width(DEFAULT_PADDING_HALF)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt index a4c90d30dd..b71d090a4e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt @@ -35,7 +35,7 @@ fun ComposeVoiceView( ) { val progress = rememberSaveable { mutableStateOf(0) } val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) } - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Box { Box( Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index ce34ecf0c3..0c4efa7d0d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -30,8 +30,8 @@ fun ContextItemView( cancelContextItem: () -> Unit ) { val sent = contextItem.chatDir.sent - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage @Composable fun MessageText(attachment: ImageResource?, lines: Int) { 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 a6565fde8f..76308d22dc 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 @@ -27,6 +27,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -233,13 +234,15 @@ fun GroupChatInfoLayout( } else { SendReceiptsOptionDisabled() } - // Should come from API - val type = remember { BackgroundImageType.default } - val theme = remember { ThemeOverrides(CurrentColors.value.base, ThemeColors()) } + + val theme = remember { (chat.chatInfo as ChatInfo.Group).groupInfo.uiTheme ?: ThemeOverrides() } WallpaperButton { ModalManager.end.showModal { - WallpaperEditor(type, theme) { type, theme -> - // apply to chat + WallpaperEditor(theme) { type, theme -> + withBGApi { + controller.apiSetChatUITheme(chat.remoteHostId, chat.id, theme.copy(wallpaper = theme.wallpaper.withFilledWallpaperPath())) + chatModel.updateChatInfo(chat.remoteHostId, (chat.chatInfo as ChatInfo.Group).copy(groupInfo = chat.chatInfo.groupInfo.copy(uiTheme = theme.copy(wallpaper = theme.wallpaper.withFilledWallpaperPath())))) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index e3008f36b3..5b95bfb8e1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -83,8 +83,8 @@ fun CIGroupInvitationView( } } - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { inProgress.value = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 318a8a6a05..0a942c001d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.ui.theme.appColors import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -137,7 +138,7 @@ fun DecryptionErrorItemFixButton( onClick: () -> Unit, syncSupported: Boolean ) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), @@ -184,7 +185,7 @@ fun DecryptionErrorItem( ci: ChatItem, onClick: () -> Unit ) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index e59c5f1370..43f40211e8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -151,8 +151,8 @@ private fun VoiceLayout( } when { hasText -> { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Spacer(Modifier.width(6.dp)) VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) Row(verticalAlignment = Alignment.CenterVertically) { @@ -220,8 +220,8 @@ private fun PlayPauseButton( pause: () -> Unit, longClick: () -> Unit ) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.drawRingModifier(angle, strokeColor, strokeWidth), color = if (sent) sentColor else receivedColor, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 7514b6e280..cc9cde15ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -18,8 +18,8 @@ import chat.simplex.common.ui.theme.* @Composable fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sent = ci.chatDir.sent - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), color = if (sent) sentColor else receivedColor, 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 8edc23c81c..0036a40f38 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 @@ -88,8 +88,8 @@ fun FramedItemView( @Composable fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Row( Modifier .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) @@ -121,8 +121,8 @@ fun FramedItemView( @Composable fun ciQuoteView(qi: CIQuote) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Row( Modifier .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) @@ -187,8 +187,8 @@ fun FramedItemView( val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Box(Modifier .clip(RoundedCornerShape(18.dp)) .background( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index 7be0cc2f6c..895de8ebff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -17,8 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MsgErrorType -import chat.simplex.common.ui.theme.CurrentColors -import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -51,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT @Composable fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 0e2e8867cb..502565cef3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -21,8 +21,8 @@ import kotlinx.datetime.Clock @Composable fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), color = if (ci.chatDir.sent) sentColor else receivedColor, 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 6641fc8ef9..21009556a3 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 @@ -29,7 +29,7 @@ enum class PredefinedBackgroundImage( val tint: Map, val colors: Map, ) { - @SerialName("cat") CAT(MR.images.background_cat, "simplex_cat", MR.strings.background_cat, 0.5f, + @SerialName("cats") CAT(MR.images.background_cat, "cats", 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), mapOf( @@ -44,7 +44,7 @@ enum class PredefinedBackgroundImage( ) ) ), - @SerialName("hearts") HEARTS(MR.images.background_hearts, "simplex_hearts", MR.strings.background_hearts, 0.5f, + @SerialName("hearts") HEARTS(MR.images.background_hearts, "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), mapOf( @@ -56,7 +56,7 @@ enum class PredefinedBackgroundImage( ) ) ), - @SerialName("school") SCHOOL(MR.images.background_school, "simplex_school", MR.strings.background_school, 0.5f, + @SerialName("school") SCHOOL(MR.images.background_school, "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), mapOf( @@ -68,7 +68,7 @@ enum class PredefinedBackgroundImage( ) ) ), - @SerialName("internet") INTERNET(MR.images.background_internet, "simplex_internet", MR.strings.background_internet, 0.5f, + @SerialName("internet") INTERNET(MR.images.background_internet, "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), mapOf( @@ -80,7 +80,7 @@ enum class PredefinedBackgroundImage( ) ) ), - @SerialName("space") SPACE(MR.images.background_space, "simplex_space", MR.strings.background_space, 0.5f, + @SerialName("space") SPACE(MR.images.background_space, "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), mapOf( @@ -92,7 +92,7 @@ enum class PredefinedBackgroundImage( ) ) ), - @SerialName("pets") PETS(MR.images.background_pets, "simplex_pets", MR.strings.background_pets, 0.5f, + @SerialName("pets") PETS(MR.images.background_pets, "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), mapOf( @@ -104,7 +104,7 @@ enum class PredefinedBackgroundImage( ) ) ), - @SerialName("rabbit") RABBIT(MR.images.background_rabbit, "simplex_rabbit", MR.strings.background_rabbit, 0.5f, + @SerialName("rabbit") RABBIT(MR.images.background_rabbit, "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), mapOf( @@ -146,9 +146,14 @@ sealed class BackgroundImageType { val res = if (this is Repeated) { PredefinedBackgroundImage.from(filename)!!.res.toComposeImageBitmap()!! } else { - File(getBackgroundImageFilePath(filename)).inputStream().use { loadImageBitmap(it) } + try { + // In case of unintentional image deletion don't crash the app + File(getBackgroundImageFilePath(filename)).inputStream().use { loadImageBitmap(it) } + } catch (e: Exception) { + null + } } - cachedImage = filename to res + cachedImage = if (res != null) filename to res else null res } } @@ -177,9 +182,9 @@ sealed class BackgroundImageType { if (this is Repeated) { PredefinedBackgroundImage.from(filename)!!.tint[theme]!! } else if (this is Static && scaleType == BackgroundImageScaleType.REPEAT) { - MaterialTheme.colors.primary + Color.Transparent } else { - MaterialTheme.colors.background.copy(0.9f) + Color.Transparent } companion object { @@ -187,6 +192,14 @@ sealed class BackgroundImageType { get() = PredefinedBackgroundImage.CAT.toType() private var cachedImage: Pair? = null + + fun from(wallpaper: ThemeWallpaper): BackgroundImageType? { + return if (wallpaper.preset != null) { + Repeated(wallpaper.preset, wallpaper.scale) + } else { + Static(wallpaper.imageFile ?: return null, wallpaper.scale, wallpaper.scaleType ?: BackgroundImageScaleType.FILL) + } + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt index 2fb27e29b1..b26a047b0e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt @@ -51,7 +51,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co @Composable fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { val theme = CurrentColors.collectAsState() - val titleColor = CurrentColors.collectAsState().value.appColors.title + val titleColor = MaterialTheme.appColors.title val brush = if (theme.value.base == DefaultTheme.SIMPLEX) Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) else // color is not updated when changing themes if I pass null here diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index 93d0a56766..20b8f7c09f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt @@ -76,7 +76,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? { @Composable fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor), verticalAlignment = Alignment.CenterVertically 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 56fb058aeb..ce822a1969 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 @@ -306,10 +306,9 @@ fun saveBackgroundImage(image: ImageBitmap): String { return destFile.name } -// TODO: removes all background for all themes. Should remove only unused -fun removeBackgroundImages(except: String? = null) { +fun removeBackgroundImage(fileName: String? = null) { File(getBackgroundImageFilePath("_")).parentFile.listFiles()?.forEach { - if (it.name != except) it.delete() + if (it.name == fileName) 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 8b563da732..7838844776 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 @@ -29,6 +29,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.ui.theme.isSystemInDarkTheme import chat.simplex.common.views.chat.item.PreviewChatItemView +import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector import chat.simplex.res.MR import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString @@ -77,8 +78,8 @@ object AppearanceScope { theme: DefaultTheme, backgroundImage: ImageBitmap?, backgroundImageType: BackgroundImageType?, - backgroundColor: Color? = CurrentColors.value.wallpaper.background, - tintColor: Color? = CurrentColors.value.wallpaper.tint, + backgroundColor: Color? = MaterialTheme.wallpaper.background, + tintColor: Color? = MaterialTheme.wallpaper.tint, withMessages: Boolean = true ) { val themeBackgroundColor = MaterialTheme.colors.background @@ -103,6 +104,121 @@ object AppearanceScope { } } + @Composable + fun WallpaperPresetSelector( + selectedBackground: BackgroundImageType?, + baseTheme: DefaultTheme, + onColorChange: (ThemeColor, Color?) -> Unit, + onTypeChange: (BackgroundImageType?) -> Unit + ) { + val resetColors = { + onColorChange(ThemeColor.WALLPAPER_BACKGROUND, null) + onColorChange(ThemeColor.WALLPAPER_TINT, null) + } + + val cornerRadius = 22 + fun setBackground(type: BackgroundImageType?) { + if (type is BackgroundImageType.Static || CurrentColors.value.wallpaper.type is BackgroundImageType.Static) { + resetColors() + } + onTypeChange(type) + } + + @Composable + fun Plus() { + Icon(painterResource(MR.images.ic_add), null, Modifier.size(25.dp), tint = MaterialTheme.colors.primary) + } + + val backgrounds = PredefinedBackgroundImage.entries.toList() + + 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)) + .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius)) + .clickable { setBackground(background?.toType()) }, + contentAlignment = Alignment.Center + ) { + if (background != null) { + val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() } + ChatThemePreview(baseTheme, backgroundImage, background.toType(), withMessages = false) + } + } + } + + @Composable + fun OwnBackgroundItem(type: BackgroundImageType?) { + val backgroundImage = selectedBackground?.image + val checked = type is BackgroundImageType.Static && backgroundImage != null + val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val filename = saveBackgroundImage(to) + if (filename != null) { + setBackground(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL)) + } + } + } + Box( + Modifier + .size(width, height) + .background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius)) + .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius)) + .clickable { + withLongRunningApi { importBackgroundImageLauncher.launch("image/*") } + }, + contentAlignment = Alignment.Center + ) { + if (checked) { + ChatThemePreview(baseTheme, backgroundImage, type, withMessages = false) + } else { + Plus() + } + } + } + + item { + BackgroundItem(null) + } + items(items = backgrounds) { background -> + BackgroundItem(background) + } + item { + OwnBackgroundItem(MaterialTheme.wallpaper.type) + } + } + val backgroundImage = selectedBackground?.image + ChatThemePreview(baseTheme, backgroundImage, selectedBackground) + + if (appPlatform.isDesktop) { + val itemWidth = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 + val itemHeight = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2) / 4 + val rows = ceil((PredefinedBackgroundImage.entries.size + 2) / 4f).roundToInt() + LazyVerticalGrid( + columns = GridCells.Fixed(4), + Modifier.height(itemHeight * rows + DEFAULT_PADDING_HALF * (rows - 1) + DEFAULT_PADDING * 2), + contentPadding = PaddingValues(DEFAULT_PADDING), + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + ) { + gridContent(itemWidth, itemHeight) + } + } else { + LazyHorizontalGrid( + rows = GridCells.Fixed(1), + Modifier.height(80.dp + DEFAULT_PADDING * 2), + contentPadding = PaddingValues(DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + ) { + gridContent(80.dp, 80.dp) + } + } + } + @Composable fun ThemesSection( systemDarkTheme: SharedPreference, @@ -110,113 +226,21 @@ object AppearanceScope { editColor: (ThemeColor, Color) -> Unit ) { val currentTheme by CurrentColors.collectAsState() - val systemDark = isSystemInDarkTheme() + val baseTheme = CurrentColors.value.base + val backgroundImageType = MaterialTheme.wallpaper.type SectionView(stringResource(MR.strings.settings_section_title_themes)) { - val selectedBackground = CurrentColors.collectAsState().value.wallpaper.type - val cornerRadius = 22 - fun setBackground(type: BackgroundImageType?) { - 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) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + WallpaperPresetSelector( + selectedBackground = backgroundImageType, + baseTheme = currentTheme.base, + onColorChange = { name, color -> + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + }, + onTypeChange = { type -> + removeBackgroundImage(backgroundImageType?.filename) + ThemeManager.saveAndApplyBackgroundImage(baseTheme, type) } - ThemeManager.saveAndApplyBackgroundImage(type, systemDark) - removeBackgroundImages(type?.filename) - } - - @Composable - fun Plus() { - Icon(painterResource(MR.images.ic_add), null, Modifier.size(25.dp), tint = MaterialTheme.colors.primary) - } - - val backgrounds = PredefinedBackgroundImage.entries.toList() - - 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)) - .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius)) - .clickable { setBackground(background?.toType()) }, - contentAlignment = Alignment.Center - ) { - if (background != null) { - val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() } - ChatThemePreview(CurrentColors.value.base, backgroundImage, background.toType(), withMessages = false) - } - } - } - - @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) - if (filename != null) { - setBackground(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL)) - } - } - } - Box( - Modifier - .size(width, height) - .background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius)) - .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius)) - .clickable { - withLongRunningApi { importBackgroundImageLauncher.launch("image/*") } - }, - contentAlignment = Alignment.Center - ) { - if (checked) { - ChatThemePreview(CurrentColors.value.base, backgroundImage, type, withMessages = false) - } else { - Plus() - } - } - } - - item { - BackgroundItem(null) - } - items(items = backgrounds) { background -> - BackgroundItem(background) - } - item { - OwnBackgroundItem(CurrentColors.collectAsState().value.wallpaper.type) - } - } - val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image - val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - ChatThemePreview(CurrentColors.value.base, backgroundImage, backgroundImageType) - - if (appPlatform.isDesktop) { - val itemWidth = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 - val itemHeight = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2) / 4 - val rows = ceil((PredefinedBackgroundImage.entries.size + 2) / 4f).roundToInt() - LazyVerticalGrid( - columns = GridCells.Fixed(4), - Modifier.height(itemHeight * rows + DEFAULT_PADDING_HALF * (rows - 1) + DEFAULT_PADDING * 2), - contentPadding = PaddingValues(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), - horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), - ) { - gridContent(itemWidth, itemHeight) - } - } else { - LazyHorizontalGrid( - rows = GridCells.Fixed(1), - Modifier.height(80.dp + DEFAULT_PADDING * 2), - contentPadding = PaddingValues(DEFAULT_PADDING), - horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), - ) { - gridContent(80.dp, 80.dp) - } - } + ) val darkTheme = isSystemInDarkTheme() val state = remember { derivedStateOf { currentTheme.name } } @@ -240,29 +264,30 @@ object AppearanceScope { val currentTheme by CurrentColors.collectAsState() AppBarTitle(stringResource(MR.strings.customize_theme_title)) - - val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image - val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type - ChatThemePreview(CurrentColors.value.base, backgroundImage, backgroundImageType) + val backgroundImage = MaterialTheme.wallpaper.type?.image + val backgroundImageType = MaterialTheme.wallpaper.type + val baseTheme = CurrentColors.value.base + ChatThemePreview(baseTheme, backgroundImage, backgroundImageType) SectionSpacer() if (backgroundImageType != null) { - 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) - } - ) + SectionView(stringResource(MR.strings.settings_section_title_wallpaper).uppercase()) { + WallpaperSetupView( + backgroundImageType, + baseTheme, + MaterialTheme.wallpaper.background, + MaterialTheme.wallpaper.tint, + showPresetSelection = false, + editColor, + onColorChange = { name, color -> + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + }, + onTypeChange = { type -> + removeBackgroundImage(backgroundImageType.filename) + ThemeManager.saveAndApplyBackgroundImage(baseTheme, type) + } + ) + } SectionDividerSpaced(maxTopPadding = true) } @@ -341,7 +366,7 @@ object AppearanceScope { if (to != null) { val theme = getThemeFromUri(to) if (theme != null) { - ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + ThemeManager.saveAndApplyThemeOverrides(theme) } } } @@ -361,8 +386,8 @@ object AppearanceScope { theme: DefaultTheme, backgroundImageType: BackgroundImageType?, backgroundImage: ImageBitmap?, - previewBackgroundColor: Color? = CurrentColors.value.wallpaper.background, - previewTintColor: Color? = CurrentColors.value.wallpaper.tint, + previewBackgroundColor: Color? = MaterialTheme.wallpaper.background, + previewTintColor: Color? = MaterialTheme.wallpaper.tint, onColorChange: (Color) -> Unit, close: () -> Unit, ) { @@ -378,7 +403,6 @@ object AppearanceScope { SectionSpacer() } - val isInDarkTheme = isInDarkTheme() var currentColor by remember { mutableStateOf(initialColor) } ColorPicker(initialColor) { currentColor = it @@ -513,93 +537,60 @@ fun WallpaperSetupView( 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) + if (showPresetSelection) { + WallpaperPresetSelector( + selectedBackground = backgroundImageType, + baseTheme = theme, + onColorChange = onColorChange, + onTypeChange = onTypeChange, + ) + } + + 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)) } - 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.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 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 != 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) } - - 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) - } + 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) } } } 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 c7276353b7..31cdf452d9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1035,7 +1035,6 @@ THEMES Profile images Wallpaper - Image MESSAGES AND FILES CALLS Network connection @@ -1538,8 +1537,6 @@ Tint - None - Choose… Cat Hearts School diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 0ac6d1f048..26eae2e9e0 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -13,7 +13,7 @@ actual val dataDir: File = File(desktopPlatform.dataPath) actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files") actual val appFilesDir: File = filesDir -actual val appearanceDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_assets" + File.separator + "wallpapers") +actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_assets" + File.separator + "wallpapers") actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" 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 05a62fda94..a60575c2a3 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,6 +4,7 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionView import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier @@ -26,16 +27,16 @@ 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 + val baseTheme = CurrentColors.collectAsState().value.base + val backgroundImage = MaterialTheme.wallpaper.type?.image + val backgroundImageType = MaterialTheme.wallpaper.type AppearanceScope.AppearanceLayout( m.controller.appPrefs.appLanguage, m.controller.appPrefs.systemDarkTheme, showSettingsModal = showSettingsModal, editColor = { name, initialColor -> ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, theme, backgroundImageType, backgroundImage, onColorChange = { color -> ThemeManager.saveAndApplyThemeColor(name, color, darkTheme) }, close = close) + ColorEditor(name, initialColor, baseTheme, backgroundImageType, backgroundImage, onColorChange = { color -> ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) }, close = close) } }, )