mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 22:55:48 +00:00
changes
This commit is contained in:
@@ -91,6 +91,12 @@ kotlin {
|
||||
|
||||
// Calls lifecycle listener
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
|
||||
|
||||
implementation("com.github.SmartToolFactory:Compose-Color-Picker-Bundle:1.2.0") {
|
||||
// This library doesn't exist anymore. Excluding it helps without making any problems
|
||||
exclude(group = "com.github.SmartToolFactory", module = "Compose-Image-Cropper")
|
||||
}
|
||||
implementation("com.github.SmartToolFactory:Compose-Extended-Colors:1.0.0-alpha07")
|
||||
}
|
||||
}
|
||||
val desktopMain by getting {
|
||||
|
||||
@@ -16,12 +16,14 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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
|
||||
@@ -36,6 +38,14 @@ 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
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -151,8 +161,6 @@ fun AppearanceScope.AppearanceLayout(
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
BackgroundImageSection(showSettingsModal)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
@@ -177,3 +185,83 @@ fun PreviewAppearanceSettings() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
ColorPickerRectHueSaturationHSV(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
.padding(horizontal = DEFAULT_PADDING * 2),
|
||||
initialColor = initialColor,
|
||||
onColorChange = { color: Color, _ ->
|
||||
onColorChanged(color)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ColorPickerRectHueSaturationHSV(
|
||||
modifier: Modifier = Modifier,
|
||||
selectionRadius: Dp = 8.dp,
|
||||
initialColor: Color,
|
||||
onColorChange: (Color, String) -> Unit
|
||||
) {
|
||||
|
||||
val hsvArray = ColorUtil.colorToHSV(initialColor)
|
||||
|
||||
var hue by remember { mutableStateOf(hsvArray[0]) }
|
||||
var saturation by remember { mutableStateOf(hsvArray[1]) }
|
||||
var value by remember { mutableStateOf(hsvArray[2]) }
|
||||
var alpha by remember { mutableStateOf(initialColor.alpha) }
|
||||
|
||||
val currentColor =
|
||||
Color.hsv(hue = hue, saturation = saturation, value = value, alpha = alpha)
|
||||
|
||||
var colorModel by remember { mutableStateOf(ColorModel.HSV) }
|
||||
|
||||
onColorChange(currentColor, ColorUtil.colorToHexAlpha(currentColor))
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
SelectorRectHueSaturationHSV(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(4 / 3f),
|
||||
hue = hue,
|
||||
saturation = saturation,
|
||||
selectionRadius = selectionRadius,
|
||||
onChange = { h, s ->
|
||||
hue = h
|
||||
saturation = s
|
||||
},
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
SliderCircleColorDisplayValueHSV(
|
||||
hue = hue,
|
||||
saturation = saturation,
|
||||
value = value,
|
||||
alpha = alpha,
|
||||
onValueChange = {
|
||||
value = it
|
||||
},
|
||||
onAlphaChange = {
|
||||
alpha = it
|
||||
}
|
||||
)
|
||||
|
||||
// Produce crash in runtime. Probably because it relies on old Compose
|
||||
/*ColorDisplayExposedSelectionMenu(
|
||||
color = currentColor,
|
||||
colorModel = colorModel,
|
||||
onColorModelChange = {
|
||||
colorModel = it
|
||||
}
|
||||
)*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ object ChatModel {
|
||||
val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000)
|
||||
val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = ProcessedErrors(20_000)
|
||||
|
||||
val backgroundImage by lazy { mutableStateOf(getBackgroundImageOrDefault()) }
|
||||
val backgroundImage: MutableState<ImageBitmap?> by lazy { mutableStateOf(getBackgroundImageOrDefault()) }
|
||||
|
||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||
currentUser.value
|
||||
|
||||
@@ -175,17 +175,17 @@ class AppPreferences {
|
||||
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
||||
}, settingsThemes)
|
||||
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
|
||||
private val _backgroundImageType = mkStrPreference(SHARED_PREFS_BACKGROUND_IMAGE, json.encodeToString(BackgroundImageType.default))
|
||||
val backgroundImageType: SharedPreference<BackgroundImageType> = SharedPreference(
|
||||
get = fun(): BackgroundImageType {
|
||||
val value = _backgroundImageType.get() ?: return BackgroundImageType.default
|
||||
private val _backgroundImageType = mkStrPreference(SHARED_PREFS_BACKGROUND_IMAGE, null)
|
||||
val backgroundImageType: SharedPreference<BackgroundImageType?> = SharedPreference(
|
||||
get = fun(): BackgroundImageType? {
|
||||
val value = _backgroundImageType.get() ?: return null
|
||||
return try {
|
||||
json.decodeFromString(value)
|
||||
} catch (e: Throwable) {
|
||||
BackgroundImageType.default
|
||||
null
|
||||
}
|
||||
},
|
||||
set = fun(type: BackgroundImageType) { _backgroundImageType.set(json.encodeToString(type)) }
|
||||
set = fun(type: BackgroundImageType?) { _backgroundImageType.set(json.encodeToString(type)) }
|
||||
)
|
||||
|
||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||
|
||||
@@ -48,13 +48,15 @@ enum class DefaultTheme {
|
||||
data class AppColors(
|
||||
val title: Color,
|
||||
val sentMessage: Color,
|
||||
val receivedMessage: Color
|
||||
val receivedMessage: Color,
|
||||
val wallpaperBackground: Color?,
|
||||
val wallpaperTint: Color?,
|
||||
)
|
||||
|
||||
enum class ThemeColor {
|
||||
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE;
|
||||
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE, WALLPAPER_BACKGROUND, WALLPAPER_TINT;
|
||||
|
||||
fun fromColors(colors: Colors, appColors: AppColors): Color {
|
||||
fun fromColors(colors: Colors, appColors: AppColors): Color? {
|
||||
return when (this) {
|
||||
PRIMARY -> colors.primary
|
||||
PRIMARY_VARIANT -> colors.primaryVariant
|
||||
@@ -65,6 +67,8 @@ enum class ThemeColor {
|
||||
TITLE -> appColors.title
|
||||
SENT_MESSAGE -> appColors.sentMessage
|
||||
RECEIVED_MESSAGE -> appColors.receivedMessage
|
||||
WALLPAPER_BACKGROUND -> appColors.wallpaperBackground
|
||||
WALLPAPER_TINT -> appColors.wallpaperTint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +83,8 @@ enum class ThemeColor {
|
||||
TITLE -> generalGetString(MR.strings.color_title)
|
||||
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
|
||||
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
|
||||
WALLPAPER_BACKGROUND -> generalGetString(MR.strings.color_wallpaper_background)
|
||||
WALLPAPER_TINT -> generalGetString(MR.strings.color_wallpaper_tint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +102,8 @@ data class ThemeColors(
|
||||
val title: String? = null,
|
||||
val sentMessage: String? = null,
|
||||
val receivedMessage: String? = null,
|
||||
val wallpaperBackground: String? = null,
|
||||
val wallpaperTint: String? = null,
|
||||
) {
|
||||
fun toColors(base: DefaultTheme): Colors {
|
||||
val baseColors = when (base) {
|
||||
@@ -127,6 +135,8 @@ data class ThemeColors(
|
||||
title = title?.colorFromReadableHex() ?: baseColors.title,
|
||||
sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage,
|
||||
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage,
|
||||
wallpaperBackground = wallpaperBackground?.colorFromReadableHex() ?: baseColors.wallpaperBackground,
|
||||
wallpaperTint = wallpaperTint?.colorFromReadableHex() ?: baseColors.wallpaperTint,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -142,7 +152,9 @@ data class ThemeColors(
|
||||
surface = c.surface.toReadableHex(),
|
||||
title = ac.title.toReadableHex(),
|
||||
sentMessage = ac.sentMessage.toReadableHex(),
|
||||
receivedMessage = ac.receivedMessage.toReadableHex()
|
||||
receivedMessage = ac.receivedMessage.toReadableHex(),
|
||||
wallpaperBackground = ac.wallpaperBackground?.toReadableHex(),
|
||||
wallpaperTint = ac.wallpaperTint?.toReadableHex(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -152,7 +164,7 @@ data class ThemeOverrides (
|
||||
val base: DefaultTheme,
|
||||
val colors: ThemeColors
|
||||
) {
|
||||
fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides {
|
||||
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides {
|
||||
return copy(colors = when (name) {
|
||||
ThemeColor.PRIMARY -> colors.copy(primary = color)
|
||||
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
|
||||
@@ -163,6 +175,8 @@ data class ThemeOverrides (
|
||||
ThemeColor.TITLE -> colors.copy(title = color)
|
||||
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
|
||||
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
|
||||
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy(wallpaperBackground = color)
|
||||
ThemeColor.WALLPAPER_TINT -> colors.copy(wallpaperTint = color)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -210,7 +224,9 @@ val DarkColorPalette = darkColors(
|
||||
val DarkColorPaletteApp = AppColors(
|
||||
title = SimplexBlue,
|
||||
sentMessage = SentMessageColor,
|
||||
receivedMessage = Color(0x20B1B0B5)
|
||||
receivedMessage = Color(0x20B1B0B5),
|
||||
wallpaperBackground = null,
|
||||
wallpaperTint = null,
|
||||
)
|
||||
|
||||
val LightColorPalette = lightColors(
|
||||
@@ -229,7 +245,9 @@ val LightColorPalette = lightColors(
|
||||
val LightColorPaletteApp = AppColors(
|
||||
title = SimplexBlue,
|
||||
sentMessage = SentMessageColor,
|
||||
receivedMessage = Color(0x20B1B0B5)
|
||||
receivedMessage = Color(0x20B1B0B5),
|
||||
wallpaperBackground = null,
|
||||
wallpaperTint = null,
|
||||
)
|
||||
|
||||
val SimplexColorPalette = darkColors(
|
||||
@@ -249,7 +267,9 @@ val SimplexColorPalette = darkColors(
|
||||
val SimplexColorPaletteApp = AppColors(
|
||||
title = Color(0xFF267BE5),
|
||||
sentMessage = SentMessageColor,
|
||||
receivedMessage = Color(0x20B1B0B5)
|
||||
receivedMessage = Color(0x20B1B0B5),
|
||||
wallpaperBackground = null,
|
||||
wallpaperTint = null,
|
||||
)
|
||||
|
||||
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
|
||||
|
||||
@@ -125,7 +125,7 @@ object ThemeManager {
|
||||
}
|
||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
||||
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex())
|
||||
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet?.toReadableHex())
|
||||
appPrefs.themeOverrides.set(overrides)
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
||||
}
|
||||
|
||||
@@ -597,14 +597,17 @@ fun ChatLayout(
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
backgroundColor = Color.Unspecified
|
||||
) { contentPadding ->
|
||||
val backgroundImage = remember { chatModel.backgroundImage }
|
||||
val backgroundImageType = remember { appPrefs.backgroundImageType.state }
|
||||
val defaultBackgroundColor = backgroundImageType.value.defaultBackgroundColor
|
||||
val defaultTintColor = backgroundImageType.value.defaultTintColor
|
||||
val backgroundImage = remember { chatModel.backgroundImage }.value
|
||||
val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value
|
||||
val defaultBackgroundColor = backgroundImageType?.defaultBackgroundColor
|
||||
val defaultTintColor = backgroundImageType?.defaultTintColor
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.drawBehind { chatViewBackground(backgroundImage.value, backgroundImageType.value, defaultBackgroundColor, defaultTintColor) }
|
||||
.then(if (backgroundImage != null && backgroundImageType != null && defaultBackgroundColor != null && defaultTintColor != null)
|
||||
Modifier.drawBehind { chatViewBackground(backgroundImage, backgroundImageType, defaultBackgroundColor, defaultTintColor) }
|
||||
else
|
||||
Modifier)
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
ChatItemsList(
|
||||
|
||||
@@ -861,7 +861,7 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
||||
|
||||
@@ -9,9 +9,8 @@ import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -20,13 +19,13 @@ import kotlin.math.*
|
||||
|
||||
@Serializable
|
||||
enum class PredefinedBackgroundImage(val res: ImageResource, val filename: String, val text: StringResource, val type: BackgroundImageType) {
|
||||
@SerialName("cat") CAT(MR.images.background_cat, "background_cat", MR.strings.background_cat, BackgroundImageType.Repeated(false, "background_cat", 0.5f)),
|
||||
@SerialName("hearts") HEARTS(MR.images.background_hearts, "background_hearts", MR.strings.background_hearts, BackgroundImageType.Repeated(false, "background_hearts", 0.5f)),
|
||||
@SerialName("school") SCHOOL(MR.images.background_school, "background_school", MR.strings.background_school, BackgroundImageType.Repeated(false, "background_school", 0.5f)),
|
||||
@SerialName("internet") INTERNET(MR.images.background_internet, "background_internet", MR.strings.background_internet, BackgroundImageType.Repeated(false, "background_internet", 0.5f)),
|
||||
@SerialName("space") SPACE(MR.images.background_space, "background_space", MR.strings.background_space, BackgroundImageType.Repeated(false, "background_space", 0.5f)),
|
||||
@SerialName("pets") PETS(MR.images.background_pets, "background_pets", MR.strings.background_pets, BackgroundImageType.Repeated(false, "background_pets", 0.5f)),
|
||||
@SerialName("rabbit") RABBIT(MR.images.background_rabbit, "background_rabbit", MR.strings.background_rabbit, BackgroundImageType.Repeated(false, "background_rabbit", 0.5f));
|
||||
@SerialName("cat") CAT(MR.images.background_cat, "simplex_cat", MR.strings.background_cat, BackgroundImageType.Repeated("simplex_cat", 0.5f)),
|
||||
@SerialName("hearts") HEARTS(MR.images.background_hearts, "simplex_hearts", MR.strings.background_hearts, BackgroundImageType.Repeated("simplex_hearts", 0.5f)),
|
||||
@SerialName("school") SCHOOL(MR.images.background_school, "simplex_school", MR.strings.background_school, BackgroundImageType.Repeated("simplex_school", 0.5f)),
|
||||
@SerialName("internet") INTERNET(MR.images.background_internet, "simplex_internet", MR.strings.background_internet, BackgroundImageType.Repeated("simplex_internet", 0.5f)),
|
||||
@SerialName("space") SPACE(MR.images.background_space, "simplex_space", MR.strings.background_space, BackgroundImageType.Repeated("simplex_space", 0.5f)),
|
||||
@SerialName("pets") PETS(MR.images.background_pets, "simplex_pets", MR.strings.background_pets, BackgroundImageType.Repeated("simplex_pets", 0.5f)),
|
||||
@SerialName("rabbit") RABBIT(MR.images.background_rabbit, "simplex_rabbit", MR.strings.background_rabbit, BackgroundImageType.Repeated("simplex_rabbit", 0.5f));
|
||||
|
||||
companion object {
|
||||
fun from(filename: String): PredefinedBackgroundImage? =
|
||||
@@ -45,55 +44,24 @@ enum class BackgroundImageScale(val contentScale: ContentScale, val text: String
|
||||
|
||||
@Serializable
|
||||
sealed class BackgroundImageType {
|
||||
abstract val custom: Boolean
|
||||
abstract val filename: String
|
||||
@Serializable @SerialName("repeated") data class Repeated(
|
||||
override val custom: Boolean = true,
|
||||
override val filename: String,
|
||||
val scale: Float,
|
||||
val backgroundColor: String? = null,
|
||||
val tintColor: String? = null
|
||||
): BackgroundImageType()
|
||||
|
||||
@Serializable @SerialName("static") data class Static(
|
||||
override val custom: Boolean = true,
|
||||
override val filename: String,
|
||||
val scale: BackgroundImageScale,
|
||||
val backgroundColor: String? = null,
|
||||
val tintColor: String? = null
|
||||
): BackgroundImageType()
|
||||
|
||||
val background: Color? by lazy {
|
||||
when (this) {
|
||||
is Repeated -> backgroundColor?.colorFromReadableHex()
|
||||
is Static -> backgroundColor?.colorFromReadableHex()
|
||||
}
|
||||
}
|
||||
val background: Color?
|
||||
get() = CurrentColors.value.appColors.wallpaperBackground
|
||||
|
||||
val tint: Color? by lazy {
|
||||
when (this) {
|
||||
is Repeated -> tintColor?.colorFromReadableHex()
|
||||
is Static -> tintColor?.colorFromReadableHex()
|
||||
}
|
||||
}
|
||||
val tint: Color?
|
||||
get() = CurrentColors.value.appColors.wallpaperTint
|
||||
|
||||
fun toPredefined(): PredefinedBackgroundImage? =
|
||||
when (this) {
|
||||
is Repeated -> if (!custom) PredefinedBackgroundImage.from(filename) else null
|
||||
is Static -> if (!custom) PredefinedBackgroundImage.from(filename) else null
|
||||
}
|
||||
|
||||
fun copyBackgroundColor(color: Color?): BackgroundImageType =
|
||||
when (this) {
|
||||
is Repeated -> copy(backgroundColor = color?.toReadableHex())
|
||||
is Static -> copy(backgroundColor = color?.toReadableHex())
|
||||
}
|
||||
|
||||
fun copyTintColor(color: Color?): BackgroundImageType =
|
||||
when (this) {
|
||||
is Repeated -> copy(tintColor = color?.toReadableHex())
|
||||
is Static -> copy(tintColor = color?.toReadableHex())
|
||||
}
|
||||
fun toPredefined(): PredefinedBackgroundImage? = PredefinedBackgroundImage.from(filename)
|
||||
|
||||
val defaultBackgroundColor: Color
|
||||
@Composable get() = if (this is Static) MaterialTheme.colors.background else MaterialTheme.colors.background
|
||||
@@ -104,7 +72,7 @@ sealed class BackgroundImageType {
|
||||
|
||||
companion object {
|
||||
val default: BackgroundImageType =
|
||||
Repeated(custom = false, PredefinedBackgroundImage.CAT.filename, 1f)
|
||||
Repeated(PredefinedBackgroundImage.CAT.filename, 1f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,9 +176,9 @@ fun getBackgroundImageFromUri(uri: URI, withAlertOnException: Boolean = true): P
|
||||
return null
|
||||
}
|
||||
|
||||
fun getBackgroundImageOrDefault(): ImageBitmap {
|
||||
val type = appPreferences.backgroundImageType.get()
|
||||
val res = if (type.custom) {
|
||||
fun getBackgroundImageOrDefault(filename: String? = null): ImageBitmap? {
|
||||
val type = appPreferences.backgroundImageType.get() ?: return null
|
||||
val res = if (type is BackgroundImageType.Static) {
|
||||
File(getBackgroundImageFilePath(type.filename)).inputStream().use {
|
||||
loadImageBitmap(it)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.*
|
||||
@@ -16,10 +19,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.unit.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
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.ui.theme.*
|
||||
@@ -35,6 +37,8 @@ import kotlinx.serialization.encodeToString
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
expect fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit))
|
||||
@@ -71,6 +75,169 @@ object AppearanceScope {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatThemePreview(backgroundImage: ImageBitmap?, backgroundImageType: BackgroundImageType?, withMessages: Boolean = true) {
|
||||
val themeBackgroundColor = MaterialTheme.colors.background
|
||||
val defaultBackgroundColor = backgroundImageType?.defaultBackgroundColor
|
||||
val defaultTintColor = backgroundImageType?.defaultTintColor
|
||||
Column(Modifier
|
||||
.drawBehind {
|
||||
if (backgroundImage != null && backgroundImageType != null && defaultBackgroundColor != null && defaultTintColor != null) {
|
||||
chatViewBackground(backgroundImage, backgroundImageType, defaultBackgroundColor, defaultTintColor)
|
||||
} else {
|
||||
drawRect(themeBackgroundColor)
|
||||
}
|
||||
}
|
||||
.padding(DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
if (withMessages) {
|
||||
PreviewChatItemView(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), stringResource(MR.strings.background_image_preview_hello_bob)))
|
||||
PreviewChatItemView(ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.background_image_preview_hello_alice)))
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomizeBackgroundImageView() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.choose_background_image_title))
|
||||
|
||||
val backgroundImage = remember { chatModel.backgroundImage }.value
|
||||
val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value
|
||||
if (backgroundImage != null && backgroundImageType != null) {
|
||||
ChatThemePreview(backgroundImage, backgroundImageType)
|
||||
|
||||
SectionSpacer()
|
||||
val isInDarkTheme = isInDarkTheme()
|
||||
val resetColors = {
|
||||
ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, null, isInDarkTheme)
|
||||
ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, null, isInDarkTheme)
|
||||
}
|
||||
val imageTypeState = remember {
|
||||
mutableStateOf(if (backgroundImageType is BackgroundImageType.Static) "" else backgroundImageType.filename)
|
||||
}
|
||||
val imageTypeValues = remember {
|
||||
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 res = saveBackgroundImage(to)
|
||||
if (res != null) {
|
||||
val (filename, backgroundImage) = res
|
||||
imageTypeState.value = ""
|
||||
chatModel.backgroundImage.value = backgroundImage
|
||||
appPrefs.backgroundImageType.set(BackgroundImageType.Static(filename, BackgroundImageScale.CROP))
|
||||
removeBackgroundImages(filename)
|
||||
resetColors()
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
stringResource(MR.strings.settings_section_title_background_image),
|
||||
imageTypeValues,
|
||||
imageTypeState,
|
||||
onSelected = { filename ->
|
||||
if (filename.isEmpty()) {
|
||||
withLongRunningApi { importBackgroundImageLauncher.launch("image/*") }
|
||||
} else {
|
||||
imageTypeState.value = filename
|
||||
appPrefs.backgroundImageType.set(PredefinedBackgroundImage.from(filename)!!.type)
|
||||
chatModel.backgroundImage.value = getBackgroundImageOrDefault()
|
||||
removeBackgroundImages()
|
||||
}
|
||||
}
|
||||
)
|
||||
if (backgroundImageType is BackgroundImageType.Repeated) {
|
||||
val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) }
|
||||
Row {
|
||||
Text("${state.value}", Modifier.width(50.dp))
|
||||
Slider(
|
||||
state.value,
|
||||
valueRange = 0.2f..2f,
|
||||
onValueChange = {
|
||||
appPrefs.backgroundImageType.set(backgroundImageType.copy(scale = it))
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (backgroundImageType is BackgroundImageType.Static) {
|
||||
val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) }
|
||||
val values = remember {
|
||||
BackgroundImageScale.entries.map { it to generalGetString(it.text) }
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
stringResource(MR.strings.background_image_scale),
|
||||
values,
|
||||
state,
|
||||
onSelected = { scale ->
|
||||
appPrefs.backgroundImageType.set(backgroundImageType.copy(scale = scale))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
var selectedTab by rememberSaveable { mutableStateOf(0) }
|
||||
val availableTabs = listOf(
|
||||
stringResource(MR.strings.background_image_background_color),
|
||||
stringResource(MR.strings.background_image_tint_color),
|
||||
)
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
availableTabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = {
|
||||
selectedTab = index
|
||||
},
|
||||
text = { Text(title, fontSize = 13.sp) },
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
val defaultBackgroundColor = backgroundImageType.defaultBackgroundColor
|
||||
val defaultTintColor = backgroundImageType.defaultTintColor
|
||||
if (selectedTab == 0) {
|
||||
var currentColor by remember(backgroundImageType.background) { mutableStateOf(backgroundImageType.background ?: defaultBackgroundColor) }
|
||||
ColorPicker(backgroundImageType.background ?: defaultBackgroundColor) {
|
||||
currentColor = it
|
||||
ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, it, isInDarkTheme)
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) })
|
||||
Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) })
|
||||
}
|
||||
} else {
|
||||
var currentColor by remember(backgroundImageType.tint) { mutableStateOf(backgroundImageType.tint ?: defaultTintColor) }
|
||||
ColorPicker(backgroundImageType.tint ?: defaultTintColor) {
|
||||
currentColor = it
|
||||
ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, it, isInDarkTheme)
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) })
|
||||
Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) })
|
||||
}
|
||||
}
|
||||
|
||||
if (backgroundImageType.background != null || backgroundImageType.tint != null) {
|
||||
SectionSpacer()
|
||||
SectionItemView(resetColors) {
|
||||
Text(generalGetString(MR.strings.reset_color), color = colors.primary)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemesSection(
|
||||
systemDarkTheme: SharedPreference<String?>,
|
||||
@@ -79,6 +246,112 @@ object AppearanceScope {
|
||||
) {
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(stringResource(MR.strings.settings_section_title_themes)) {
|
||||
val selectedBackground = remember { appPrefs.backgroundImageType.state }.value
|
||||
val cornerRadius = remember { appPreferences.profileImageCornerRadius.state }
|
||||
fun setBackground(type: BackgroundImageType?) {
|
||||
appPrefs.backgroundImageType.set(type)
|
||||
chatModel.backgroundImage.value = getBackgroundImageOrDefault()
|
||||
removeBackgroundImages(type?.filename)
|
||||
}
|
||||
@Composable
|
||||
fun Checked() {
|
||||
Box(Modifier.size(40.dp).background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.9f), RoundedCornerShape(cornerRadius.value.roundToInt())))
|
||||
Icon(painterResource(MR.images.ic_check_filled), null, Modifier.size(25.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
@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?) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(width, height)
|
||||
.background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt()))
|
||||
.clickable { setBackground(background?.type) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (background != null) {
|
||||
val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() }
|
||||
ChatThemePreview(backgroundImage, background.type, withMessages = false)
|
||||
}
|
||||
if ((background == null && selectedBackground == null) || (selectedBackground?.filename == background?.filename)) {
|
||||
Checked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OwnBackgroundItem(type: BackgroundImageType?) {
|
||||
val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
val res = saveBackgroundImage(to)
|
||||
if (res != null) {
|
||||
val (filename, backgroundImage) = res
|
||||
setBackground(BackgroundImageType.Static(filename, BackgroundImageScale.CROP))
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.size(width, height)
|
||||
.background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt()))
|
||||
.clickable {
|
||||
withLongRunningApi { importBackgroundImageLauncher.launch("image/*") }
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val backgroundImage = remember { chatModel.backgroundImage }.value
|
||||
if (type is BackgroundImageType.Static && backgroundImage != null) {
|
||||
ChatThemePreview(backgroundImage, type, withMessages = false)
|
||||
Checked()
|
||||
} else {
|
||||
Plus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
BackgroundItem(null)
|
||||
}
|
||||
items(items = backgrounds) { background ->
|
||||
BackgroundItem(background)
|
||||
}
|
||||
item {
|
||||
OwnBackgroundItem(remember { appPrefs.backgroundImageType.state }.value)
|
||||
}
|
||||
}
|
||||
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) / 3
|
||||
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(70.dp + DEFAULT_PADDING * 2),
|
||||
contentPadding = PaddingValues(DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
|
||||
) {
|
||||
gridContent(50.dp, 70.dp)
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemView(showSettingsModal{ _ -> CustomizeBackgroundImageView() }) { Text(stringResource(MR.strings.choose_background_image_title)) }
|
||||
|
||||
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val state = remember { derivedStateOf { currentTheme.name } }
|
||||
ThemeSelector(state) {
|
||||
@@ -93,169 +366,6 @@ object AppearanceScope {
|
||||
SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackgroundImageSection(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) {
|
||||
SectionItemView(showSettingsModal{ _ -> CustomizeBackgroundImageView() }) { Text(stringResource(MR.strings.choose_background_image_title)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomizeBackgroundImageView() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.choose_background_image_title))
|
||||
|
||||
val backgroundImage = remember { chatModel.backgroundImage }
|
||||
val backgroundImageType = remember { appPrefs.backgroundImageType.state }
|
||||
val defaultBackgroundColor = backgroundImageType.value.defaultBackgroundColor
|
||||
val defaultTintColor = backgroundImageType.value.defaultTintColor
|
||||
Column(Modifier
|
||||
.drawBehind { chatViewBackground(backgroundImage.value, backgroundImageType.value, defaultBackgroundColor, defaultTintColor) }
|
||||
.padding(DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
PreviewChatItemView(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), stringResource(MR.strings.background_image_preview_hello_bob)))
|
||||
PreviewChatItemView(ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.background_image_preview_hello_alice)))
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
val resetColors = { appPrefs.backgroundImageType.set(backgroundImageType.value.copyBackgroundColor(null).copyTintColor(null)) }
|
||||
|
||||
val imageTypeState = remember {
|
||||
val type = appPrefs.backgroundImageType.get()
|
||||
mutableStateOf(if (type.custom) "" else type.filename)
|
||||
}
|
||||
val imageTypeValues = remember {
|
||||
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 res = saveBackgroundImage(to)
|
||||
if (res != null) {
|
||||
val (filename, backgroundImage) = res
|
||||
imageTypeState.value = ""
|
||||
chatModel.backgroundImage.value = backgroundImage
|
||||
appPrefs.backgroundImageType.set(BackgroundImageType.Static(custom = true, filename, BackgroundImageScale.CROP, null))
|
||||
removeBackgroundImages(filename)
|
||||
resetColors()
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
stringResource(MR.strings.settings_section_title_background_image),
|
||||
imageTypeValues,
|
||||
imageTypeState,
|
||||
onSelected = { filename ->
|
||||
if (filename.isEmpty()) {
|
||||
withLongRunningApi { importBackgroundImageLauncher.launch("image/*") }
|
||||
} else {
|
||||
imageTypeState.value = filename
|
||||
appPrefs.backgroundImageType.set(PredefinedBackgroundImage.from(filename)!!.type)
|
||||
chatModel.backgroundImage.value = getBackgroundImageOrDefault()
|
||||
removeBackgroundImages()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val type = backgroundImageType.value
|
||||
if (type is BackgroundImageType.Repeated) {
|
||||
val state = remember(type.scale) { mutableStateOf(type.scale) }
|
||||
val values = remember {
|
||||
listOf(
|
||||
0.25f to "0.25x",
|
||||
0.5f to "0.5x",
|
||||
0.75f to "0.75x",
|
||||
1f to "1x",
|
||||
)
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
stringResource(MR.strings.background_image_scale),
|
||||
values,
|
||||
state,
|
||||
onSelected = { scale ->
|
||||
appPrefs.backgroundImageType.set(type.copy(scale = scale))
|
||||
}
|
||||
)
|
||||
} else if (type is BackgroundImageType.Static) {
|
||||
val state = remember(type.scale) { mutableStateOf(type.scale) }
|
||||
val values = remember {
|
||||
BackgroundImageScale.entries.map { it to generalGetString(it.text) }
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
stringResource(MR.strings.background_image_scale),
|
||||
values,
|
||||
state,
|
||||
onSelected = { scale ->
|
||||
appPrefs.backgroundImageType.set(type.copy(scale = scale))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
var selectedTab by rememberSaveable { mutableStateOf(0) }
|
||||
val availableTabs = listOf(
|
||||
stringResource(MR.strings.background_image_background_color),
|
||||
stringResource(MR.strings.background_image_tint_color),
|
||||
)
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
availableTabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = {
|
||||
selectedTab = index
|
||||
},
|
||||
text = { Text(title, fontSize = 13.sp) },
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab == 0) {
|
||||
var currentColor by remember(backgroundImageType.value.background) { mutableStateOf(backgroundImageType.value.background ?: defaultBackgroundColor) }
|
||||
ColorPicker(backgroundImageType.value.background ?: defaultBackgroundColor) {
|
||||
currentColor = it
|
||||
appPrefs.backgroundImageType.set(appPrefs.backgroundImageType.get().copyBackgroundColor(currentColor))
|
||||
}
|
||||
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) })
|
||||
Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) })
|
||||
}
|
||||
} else {
|
||||
var currentColor by remember(backgroundImageType.value.tint) { mutableStateOf(backgroundImageType.value.tint ?: defaultTintColor) }
|
||||
ColorPicker(backgroundImageType.value.tint ?: defaultTintColor) {
|
||||
currentColor = it
|
||||
appPrefs.backgroundImageType.set(appPrefs.backgroundImageType.get().copyTintColor(currentColor))
|
||||
}
|
||||
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Text(currentColor.toReadableHex(), modifier = Modifier.clickable { clipboard.shareText(currentColor.toReadableHex()) })
|
||||
Text("#" + currentColor.toReadableHex().substring(3), modifier = Modifier.clickable { clipboard.shareText("#" + currentColor.toReadableHex().substring(3)) })
|
||||
}
|
||||
}
|
||||
|
||||
if (backgroundImageType.value.background != null || backgroundImageType.value.tint != null) {
|
||||
SectionSpacer()
|
||||
SectionItemView(resetColors) {
|
||||
Text(generalGetString(MR.strings.reset_color), color = colors.primary)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) {
|
||||
ColumnWithScrollBar(
|
||||
@@ -265,6 +375,29 @@ object AppearanceScope {
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.customize_theme_title))
|
||||
|
||||
val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value
|
||||
val backgroundImage = remember { chatModel.backgroundImage }
|
||||
ChatThemePreview(backgroundImage.value, backgroundImageType)
|
||||
SectionSpacer()
|
||||
|
||||
if (backgroundImageType != null) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) {
|
||||
val wallpaperBackgroundColor = currentTheme.appColors.wallpaperBackground ?: backgroundImageType.defaultBackgroundColor
|
||||
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 = currentTheme.appColors.wallpaperTint ?: backgroundImageType.defaultTintColor
|
||||
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)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
SectionView(stringResource(MR.strings.theme_colors_section_title)) {
|
||||
SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) {
|
||||
val title = generalGetString(MR.strings.color_primary)
|
||||
@@ -364,16 +497,38 @@ object AppearanceScope {
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AppBarTitle(name.text)
|
||||
|
||||
val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT)
|
||||
if (supportedLiveChange) {
|
||||
val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value
|
||||
val backgroundImage = remember { chatModel.backgroundImage }
|
||||
ChatThemePreview(backgroundImage.value, backgroundImageType)
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
val isInDarkTheme = isInDarkTheme()
|
||||
var currentColor by remember { mutableStateOf(initialColor) }
|
||||
ColorPicker(initialColor) {
|
||||
currentColor = it
|
||||
if (supportedLiveChange) {
|
||||
ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme)
|
||||
}
|
||||
}
|
||||
val savedColor = remember { mutableStateOf(initialColor) }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (currentColor != savedColor.value) {
|
||||
// Rollback changes since they weren't saved
|
||||
ThemeManager.saveAndApplyThemeColor(name, savedColor.value, isInDarkTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
val isInDarkTheme = isInDarkTheme()
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme)
|
||||
savedColor.value = currentColor
|
||||
close()
|
||||
},
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
@@ -384,17 +539,7 @@ object AppearanceScope {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
ClassicColorPicker(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
color = HsvColor.from(color = initialColor), showAlphaBar = true,
|
||||
onColorChanged = { color: HsvColor ->
|
||||
onColorChanged(color.toColor())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
|
||||
@@ -472,3 +617,5 @@ object AppearanceScope {
|
||||
//}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit)
|
||||
|
||||
@@ -1534,6 +1534,8 @@
|
||||
<string name="color_title">Title</string>
|
||||
<string name="color_sent_message">Sent message</string>
|
||||
<string name="color_received_message">Received message</string>
|
||||
<string name="color_wallpaper_background">Wallpaper background</string>
|
||||
<string name="color_wallpaper_tint">Wallpaper tint</string>
|
||||
|
||||
<!-- Backgrounds -->
|
||||
<string name="background_choose_own_image">Choose…</string>
|
||||
|
||||
@@ -9,14 +9,18 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.SharedPreference
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.defaultLocale
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.ThemeColor
|
||||
import chat.simplex.common.views.helpers.*
|
||||
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 dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.Locale
|
||||
@@ -68,8 +72,18 @@ fun AppearanceScope.AppearanceLayout(
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
BackgroundImageSection(showSettingsModal)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
ClassicColorPicker(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
color = HsvColor.from(color = initialColor), showAlphaBar = true,
|
||||
onColorChanged = { color: HsvColor ->
|
||||
onColorChanged(color.toColor())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user