mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
Merge branch 'master' into ab/diff-subs
This commit is contained in:
commit
0d62d7ddfb
11 changed files with 237 additions and 41 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 = { },
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue