Merge branch 'master' into ab/diff-subs

This commit is contained in:
Evgeny Poberezkin 2024-05-28 08:06:25 +01:00
commit 0d62d7ddfb
No known key found for this signature in database
GPG key ID: 494BDDD9A28B577D
11 changed files with 237 additions and 41 deletions

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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)

View file

@ -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 = { },

View file

@ -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,
)
}
}

View file

@ -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) {

View file

@ -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<ThemeOverrides>(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)
}
}

View file

@ -146,6 +146,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
runCatching {
return yaml.decodeFromStream<ThemeOverrides>(it!!)
}.onFailure {
Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}")
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.import_theme_error),

View file

@ -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
},
)

View file

@ -1570,7 +1570,7 @@
<string name="export_theme">Export theme</string>
<string name="reset_color">Reset colors</string>
<string name="reset_single_color">Reset color</string>
<string name="theme_destination_all_profiles">All chat profiles</string>
<string name="theme_destination_app_theme">App theme</string>
<string name="color_primary">Accent</string>
<string name="color_primary_variant">Additional accent</string>
<string name="color_secondary">Secondary</string>
@ -1601,7 +1601,8 @@
<string name="wallpaper_scale_fill">Fill</string>
<string name="wallpaper_scale_fit">Fit</string>
<string name="wallpaper_advanced_settings">Advanced settings</string>
<string name="chat_theme_reset_to_global_theme">Reset to global theme</string>
<string name="chat_theme_reset_to_app_theme">Reset to app theme</string>
<string name="chat_theme_reset_to_user_theme">Reset to user theme</string>
<string name="chat_theme_set_default_theme">Set default theme</string>
<string name="chat_theme_apply_to_mode">Apply to</string>
<string name="chat_theme_apply_to_all_modes">All color modes</string>