This commit is contained in:
Avently
2024-05-07 19:10:46 +07:00
parent 0c5a464a09
commit c12340c59a
34 changed files with 885 additions and 748 deletions
@@ -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"
@@ -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)
}
},
)
@@ -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,
)
}
}
@@ -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(),
)
}
}
@@ -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)
@@ -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
}
@@ -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))
@@ -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)
}
)
}
@@ -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())
}
@@ -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)
}
)
@@ -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) }
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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))
@@ -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
@@ -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) {
@@ -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()))))
}
}
}
}
@@ -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
@@ -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),
@@ -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,
@@ -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,
@@ -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(
@@ -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),
@@ -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,
@@ -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)
}
}
}
}
@@ -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
@@ -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
@@ -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()
}
}
@@ -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>
@@ -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"
@@ -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)
}
},
)