diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 4fa865a6d3..ac131c9748 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -37,7 +37,7 @@ kotlin { api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") api("com.russhwolf:multiplatform-settings:1.1.1") - api("com.charleskorn.kaml:kaml:0.58.0") + api("com.charleskorn.kaml:kaml:0.59.0") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") //Barcode 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 40207a25ce..78d76a4b5d 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 @@ -4104,6 +4104,8 @@ val jsonShort = Json { val yaml = Yaml(configuration = YamlConfiguration( strictMode = false, encodeDefaults = false, + /** ~5.5 MB limitation since wallpaper is limited by 5 MB, see [saveWallpaperFile] */ + codePointLimit = 5500000, )) @Serializable 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 5099513884..45f656a011 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 @@ -506,6 +506,76 @@ data class ThemeModeOverride ( } ) } + + fun removeSameColors(base: DefaultTheme): ThemeModeOverride { + val c = when (base) { + DefaultTheme.LIGHT -> LightColorPalette + DefaultTheme.DARK -> DarkColorPalette + DefaultTheme.SIMPLEX -> SimplexColorPalette + DefaultTheme.BLACK -> BlackColorPalette + } + val ac = when (base) { + DefaultTheme.LIGHT -> LightColorPaletteApp + DefaultTheme.DARK -> DarkColorPaletteApp + DefaultTheme.SIMPLEX -> SimplexColorPaletteApp + DefaultTheme.BLACK -> BlackColorPaletteApp + } + val w = when (val wallpaperType = WallpaperType.from(wallpaper)) { + is WallpaperType.Preset -> { + val p = PresetWallpaper.from(wallpaperType.filename) + ThemeWallpaper( + preset = wallpaperType.filename, + scale = p?.scale ?: wallpaper?.scale, + scaleType = null, + background = p?.background?.get(base)?.toReadableHex(), + tint = p?.tint?.get(base)?.toReadableHex(), + image = null, + imageFile = null, + ) + } + is WallpaperType.Image -> { + ThemeWallpaper( + preset = null, + scale = null, + scaleType = WallpaperScaleType.FILL, + background = Color.Transparent.toReadableHex(), + tint = Color.Transparent.toReadableHex(), + image = null, + imageFile = null, + ) + } + else -> { + ThemeWallpaper() + } + } + + return copy( + colors = ThemeColors( + primary = if (colors.primary?.colorFromReadableHex() != c.primary) colors.primary else null, + primaryVariant = if (colors.primaryVariant?.colorFromReadableHex() != c.primaryVariant) colors.primaryVariant else null, + secondary = if (colors.secondary?.colorFromReadableHex() != c.secondary) colors.secondary else null, + secondaryVariant = if (colors.secondaryVariant?.colorFromReadableHex() != c.secondaryVariant) colors.secondaryVariant else null, + background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null, + surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null, + title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null, + primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null, + sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null, + sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null, + receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null, + receivedQuote = if (colors.receivedQuote?.colorFromReadableHex() != ac.receivedQuote) colors.receivedQuote else null, + ), + wallpaper = wallpaper?.copy( + preset = wallpaper.preset, + scale = if (wallpaper.scale != w.scale) wallpaper.scale else null, + scaleType = if (wallpaper.scaleType != w.scaleType) wallpaper.scaleType else null, + background = if (wallpaper.background != w.background) wallpaper.background else null, + tint = if (wallpaper.tint != w.tint) wallpaper.tint else null, + image = wallpaper.image, + imageFile = wallpaper.imageFile, + ) + ) + } + companion object { fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride = ThemeModeOverride( 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 9b1e908e6b..12b5747787 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 @@ -42,6 +42,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.datetime.Clock +import java.io.File @Composable fun ChatInfoView( @@ -719,7 +720,7 @@ fun ModalData.ChatWallpaperEditorModal(chat: Chat) { suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) { val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides() - val wallpaperFiles = listOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) + val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) var changedThemes: ThemeModeOverrides? = unchangedThemes val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) changedThemes = when (applyToMode) { @@ -727,7 +728,28 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode)) DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode)) } - changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) changedThemes else null + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) { + val light = changedThemes.light + val dark = changedThemes.dark + val currentMode = CurrentColors.value.base.mode + // same image file for both modes, copy image to make them as different files + if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) { + val imageFile = if (currentMode == DefaultThemeMode.LIGHT) { + dark.wallpaper.imageFile + } else { + light.wallpaper.imageFile + } + val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI()) + changedThemes = if (currentMode == DefaultThemeMode.LIGHT) { + changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath))) + } else { + changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath))) + } + } + changedThemes + } else { + null + } val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile wallpaperFilesToDelete.forEach(::removeWallpaperFile) 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 60f741b9af..3b3060c81a 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 @@ -339,6 +339,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: openDirectChat(chatRh, contactId, chatModel) } }, + forwardItem = { cItem, cInfo -> + chatModel.chatId.value = null + chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem) + }, updateContactStats = { contact -> withBGApi { val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) @@ -537,6 +541,7 @@ fun ChatLayout( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -619,7 +624,7 @@ fun ChatLayout( ChatItemsList( chat, unreadCount, composeState, searchValue, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy, ) @@ -888,6 +893,7 @@ fun BoxWithConstraintsScope.ChatItemsList( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -991,7 +997,7 @@ fun BoxWithConstraintsScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) + ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) } } @@ -1544,6 +1550,7 @@ fun PreviewChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1617,6 +1624,7 @@ fun PreviewGroupChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 7fbec84c4d..c195a1a299 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -59,6 +59,7 @@ fun ChatItemView( scrollToItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -68,7 +69,8 @@ fun ChatItemView( setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, developerTools: Boolean, - showViaProxy: Boolean + showViaProxy: Boolean, + preview: Boolean = false, ) { val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent @@ -260,8 +262,7 @@ fun ChatItemView( !cItem.isLiveDummy && !live ) { ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { - chatModel.chatId.value = null - chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo) + forwardItem(cInfo, cItem) showMenu.value = false }) } @@ -272,7 +273,7 @@ fun ChatItemView( if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) } - if (!(live && cItem.meta.isLive)) { + if (!(live && cItem.meta.isLive) && !preview) { DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) } val groupInfo = cItem.memberToModerate(cInfo)?.first @@ -840,6 +841,7 @@ fun PreviewChatItemView( scrollToItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -850,6 +852,7 @@ fun PreviewChatItemView( showItemDetails = { _, _ -> }, developerTools = false, showViaProxy = false, + preview = true, ) } @@ -875,6 +878,7 @@ fun PreviewChatItemViewDeletedContent() { scrollToItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -884,7 +888,8 @@ fun PreviewChatItemViewDeletedContent() { setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, developerTools = false, - showViaProxy = false + showViaProxy = false, + preview = true, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt index cd18ad1dab..b06bc46202 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt @@ -379,6 +379,9 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, b val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) val scaledWidth = (image.width * scale.scaleX).roundToInt() val scaledHeight = (image.height * scale.scaleY).roundToInt() + // Large image will cause freeze + if (image.width > 4320 || image.height > 4320) return@clipRect + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) if (scaleType == WallpaperScaleType.FIT) { if (scaledWidth < size.width) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt index 9df4c80b0e..24f9bf539b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt @@ -3,13 +3,16 @@ package chat.simplex.common.views.helpers import SectionBottomSpacer import SectionItemView import SectionSpacer +import SectionView import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme +import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.yaml import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.usersettings.* @@ -18,6 +21,7 @@ import chat.simplex.common.views.usersettings.AppearanceScope.editColor import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.serialization.encodeToString import java.net.URI @Composable @@ -134,7 +138,7 @@ fun ModalData.UserWallpaperEditor( SectionSpacer() if (!globalThemeUsed.value) { - ResetToGlobalThemeButton { + ResetToGlobalThemeButton(true) { themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) globalThemeUsed.value = true withBGApi { save(applyToMode.value, null) } @@ -202,6 +206,14 @@ fun ModalData.UserWallpaperEditor( SectionSpacer() AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) + + SectionSpacer() + ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) { + withBGApi { + themeModeOverride.value = it + save(applyToMode.value, it) + } + } } else { AdvancedSettingsButton { showMore = true } } @@ -226,7 +238,7 @@ fun ModalData.ChatWallpaperEditor( val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) { mutableStateOf( - ThemeManager.currentColors(null, if (themeModeOverride.value == ThemeModeOverride()) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + ThemeManager.currentColors(null, if (globalThemeUsed.value) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) ) } @@ -363,7 +375,7 @@ fun ModalData.ChatWallpaperEditor( SectionSpacer() if (!globalThemeUsed.value) { - ResetToGlobalThemeButton { + ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) { themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) globalThemeUsed.value = true withBGApi { save(applyToMode.value, null) } @@ -431,6 +443,14 @@ fun ModalData.ChatWallpaperEditor( SectionSpacer() AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) + + SectionSpacer() + ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) { + withBGApi { + themeModeOverride.value = it + save(applyToMode.value, it) + } + } } else { AdvancedSettingsButton { showMore = true } } @@ -440,9 +460,46 @@ fun ModalData.ChatWallpaperEditor( } @Composable -private fun ResetToGlobalThemeButton(onClick: () -> Unit) { +private fun ImportExportThemeSection(perChat: ThemeModeOverride?, perUser: ThemeModeOverrides?, save: (ThemeModeOverride) -> Unit) { + SectionView { + val theme = remember { mutableStateOf(null as String?) } + val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val themeValue = theme.value + if (themeValue != null && to != null) { + copyBytesToFile(themeValue.byteInputStream(), to) { + theme.value = null + } + } + } + SectionItemView({ + val overrides = ThemeManager.currentThemeOverridesForExport(perChat, perUser) + val lines = yaml.encodeToString(overrides).lines() + // Removing theme id without using custom serializer or data class + theme.value = lines.subList(1, lines.size).joinToString("\n") + withLongRunningApi { exportThemeLauncher.launch("simplex.theme") } + }) { + Text(generalGetString(MR.strings.export_theme), color = colors.primary) + } + val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val theme = getThemeFromUri(to) + if (theme != null) { + val res = ThemeModeOverride(mode = theme.base.mode, colors = theme.colors, wallpaper = theme.wallpaper?.importFromString()).removeSameColors(theme.base) + save(res) + } + } + } + // Can not limit to YAML mime type since it's unsupported by Android + SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) { + Text(generalGetString(MR.strings.import_theme), color = colors.primary) + } + } +} + +@Composable +private fun ResetToGlobalThemeButton(app: Boolean, onClick: () -> Unit) { SectionItemView(onClick) { - Text(stringResource(MR.strings.chat_theme_reset_to_global_theme), color = MaterialTheme.colors.primary) + Text(stringResource(if (app) MR.strings.chat_theme_reset_to_app_theme else MR.strings.chat_theme_reset_to_user_theme), color = MaterialTheme.colors.primary) } } 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 4740280bb5..884551f600 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 @@ -146,6 +146,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri runCatching { return yaml.decodeFromStream(it!!) }.onFailure { + Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}") if (withAlertOnException) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.import_theme_error), 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 ad7804de00..1de2c7c489 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 @@ -40,6 +40,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString +import java.io.File import java.net.URI import java.util.* import kotlin.collections.ArrayList @@ -277,7 +278,7 @@ object AppearanceScope { if (themeUserDestination.value == null) { ThemeManager.saveAndApplyWallpaper(baseTheme, type) } else { - val wallpaperFiles = listOf(perUserTheme.value.wallpaper?.imageFile) + val wallpaperFiles = setOf(perUserTheme.value.wallpaper?.imageFile) ThemeManager.copyFromSameThemeOverrides(type, null, perUserTheme) val wallpaperFilesToDelete = wallpaperFiles - perUserTheme.value.wallpaper?.imageFile wallpaperFilesToDelete.forEach(::removeWallpaperFile) @@ -297,17 +298,15 @@ object AppearanceScope { saveThemeToDatabase(themeUserDestination.value) } - val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> - if (to != null) { - val filename = saveWallpaperFile(to) - if (filename != null) { - if (themeUserDestination.value == null) { - removeWallpaperFile((currentTheme.wallpaper.type as? WallpaperType.Image)?.filename) - } else { - removeWallpaperFile((perUserTheme.value.type as? WallpaperType.Image)?.filename) - } - onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + val onImport = { to: URI -> + val filename = saveWallpaperFile(to) + if (filename != null) { + if (themeUserDestination.value == null) { + removeWallpaperFile((currentTheme.wallpaper.type as? WallpaperType.Image)?.filename) + } else { + removeWallpaperFile((perUserTheme.value.type as? WallpaperType.Image)?.filename) } + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) } } @@ -319,18 +318,18 @@ object AppearanceScope { ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) } - val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? -> + val onChooseType: (WallpaperType?, FileChooserLauncher) -> Unit = { type: WallpaperType?, importWallpaperLauncher: FileChooserLauncher -> when { // don't have image in parent or already selected wallpaper with custom image type is WallpaperType.Image && ((wallpaperType is WallpaperType.Image && themeUserDestination.value?.second != null && chatModel.remoteHostId() == null) || currentColors(type).wallpaper.type.image == null || - (currentColors(type).wallpaper.type.image != null && wallpaperType is WallpaperType.Image && themeUserDestination.value == null)) -> + (currentColors(type).wallpaper.type.image != null && CurrentColors.value.wallpaper.type is WallpaperType.Image && themeUserDestination.value == null)) -> withLongRunningApi { importWallpaperLauncher.launch("image/*") } type is WallpaperType.Image && themeUserDestination.value == null -> onTypeChange(currentColors(type).wallpaper.type) type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing when remote host connected */ } type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) - (themeUserDestination.value != null && themeUserDestination.value?.second?.preferredMode(!CurrentColors.value.colors.isLight)?.type != type) || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type) + (themeUserDestination.value != null && themeUserDestination.value?.second?.preferredMode(!CurrentColors.value.colors.isLight)?.type != type) || CurrentColors.value.wallpaper.type != type -> onTypeCopyFromSameTheme(type) else -> onTypeChange(type) } } @@ -340,13 +339,17 @@ object AppearanceScope { ThemeDestinationPicker(themeUserDestination) Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + WallpaperPresetSelector( selectedWallpaper = wallpaperType, baseTheme = currentTheme.base, currentColors = { type -> currentColors(type) }, - onChooseType = onChooseType, + onChooseType = { onChooseType(it, importWallpaperLauncher) }, ) val type = MaterialTheme.wallpaper.type if (type is WallpaperType.Image && (themeUserDestination.value == null || perUserTheme.value.wallpaper?.imageFile != null)) { @@ -400,7 +403,10 @@ object AppearanceScope { val user = themeUserDestination.value if (user == null) { ModalManager.start.showModal { - CustomizeThemeView(onChooseType) + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + CustomizeThemeView { onChooseType(it, importWallpaperLauncher) } } } else { ModalManager.start.showModalCloseable { close -> @@ -586,19 +592,19 @@ object AppearanceScope { @Composable fun ModalData.UserWallpaperEditorModal(remoteHostId: Long?, userId: Long, close: () -> Unit) { - val themes = remember(chatModel.currentUser.value) { chatModel.currentUser.value?.uiThemes ?: ThemeModeOverrides() } + val themes = remember(chatModel.currentUser.value) { mutableStateOf(chatModel.currentUser.value?.uiThemes ?: ThemeModeOverrides()) } val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } } val initialTheme = remember(CurrentColors.collectAsState().value.base) { - val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight) + val preferred = themes.value.preferredMode(!CurrentColors.value.colors.isLight) globalThemeUsed.value = preferred == null preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } UserWallpaperEditor( initialTheme, - applyToMode = if (themes.light == themes.dark) null else initialTheme.mode, + applyToMode = if (themes.value.light == themes.value.dark) null else initialTheme.mode, globalThemeUsed = globalThemeUsed, save = { applyToMode, newTheme -> - save(applyToMode, newTheme, themes, userId, remoteHostId) + save(applyToMode, newTheme, themes.value, userId, remoteHostId) }) KeyChangeEffect(chatModel.currentUser.value?.userId, chatModel.remoteHostId) { close() @@ -613,7 +619,7 @@ object AppearanceScope { remoteHostId: Long? ) { val unchangedThemes: ThemeModeOverrides = themes ?: ThemeModeOverrides() - val wallpaperFiles = listOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) + val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) var changedThemes: ThemeModeOverrides? = unchangedThemes val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) changedThemes = when (applyToMode) { @@ -621,7 +627,28 @@ object AppearanceScope { DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode)) DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode)) } - changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) changedThemes else null + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) { + val light = changedThemes.light + val dark = changedThemes.dark + val currentMode = CurrentColors.value.base.mode + // same image file for both modes, copy image to make them as different files + if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) { + val imageFile = if (currentMode == DefaultThemeMode.LIGHT) { + dark.wallpaper.imageFile + } else { + light.wallpaper.imageFile + } + val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI()) + changedThemes = if (currentMode == DefaultThemeMode.LIGHT) { + changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath))) + } else { + changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath))) + } + } + changedThemes + } else { + null + } val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile wallpaperFilesToDelete.forEach(::removeWallpaperFile) @@ -662,9 +689,9 @@ object AppearanceScope { } val values by remember(chatModel.users.toList()) { mutableStateOf( - listOf(null as Long? to generalGetString(MR.strings.theme_destination_all_profiles)) + listOf(null as Long? to generalGetString(MR.strings.theme_destination_app_theme)) + - chatModel.users.filter { it.user.activeUser || it.user.viewPwdHash == null }.map { + chatModel.users.filter { it.user.activeUser }.map { it.user.userId to it.user.chatViewName }, ) 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 b00f1a9438..dca3e95156 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1570,7 +1570,7 @@ Export theme Reset colors Reset color - All chat profiles + App theme Accent Additional accent Secondary @@ -1601,7 +1601,8 @@ Fill Fit Advanced settings - Reset to global theme + Reset to app theme + Reset to user theme Set default theme Apply to All color modes