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