backgrounds moved to themes. can be exported/imported

This commit is contained in:
Avently
2024-05-02 18:26:19 +07:00
parent 07fd79dcfa
commit 572dcb1556
9 changed files with 245 additions and 154 deletions
@@ -137,8 +137,6 @@ object ChatModel {
val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000)
val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = ProcessedErrors(20_000)
val backgroundImage: MutableState<ImageBitmap?> by lazy { mutableStateOf(getBackgroundImageOrDefault()) }
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
} else {
@@ -175,18 +175,6 @@ 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, null)
val backgroundImageType: SharedPreference<BackgroundImageType?> = SharedPreference(
get = fun(): BackgroundImageType? {
val value = _backgroundImageType.get() ?: return null
return try {
json.decodeFromString(value)
} catch (e: Throwable) {
null
}
},
set = fun(type: BackgroundImageType?) { _backgroundImageType.set(json.encodeToString(type)) }
)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@@ -354,7 +342,6 @@ class AppPreferences {
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
private const val SHARED_PREFS_THEMES = "Themes"
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
private const val SHARED_PREFS_BACKGROUND_IMAGE = "BackgroundImage"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
@@ -6,10 +6,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.isInNightMode
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.helpers.*
@@ -17,12 +16,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import chat.simplex.res.MR
import java.io.File
enum class DefaultTheme {
SYSTEM, LIGHT, DARK, SIMPLEX;
// Call it only with base theme, not SYSTEM
fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean {
fun hasChangedAnyColor(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Boolean {
val palette = when (this) {
SYSTEM -> return false
LIGHT -> LightColorPalette
@@ -41,7 +41,9 @@ enum class DefaultTheme {
colors.secondaryVariant != palette.secondaryVariant ||
colors.background != palette.background ||
colors.surface != palette.surface ||
appColors != appPalette
appColors != appPalette ||
appWallpaper.background != null ||
appWallpaper.tint != null
}
}
@@ -49,14 +51,18 @@ data class AppColors(
val title: Color,
val sentMessage: Color,
val receivedMessage: Color,
val wallpaperBackground: Color?,
val wallpaperTint: Color?,
)
data class AppWallpaper(
val background: Color? = null,
val tint: Color? = null,
val type: BackgroundImageType? = null
)
enum class ThemeColor {
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, appWallpaper: AppWallpaper): Color? {
return when (this) {
PRIMARY -> colors.primary
PRIMARY_VARIANT -> colors.primaryVariant
@@ -67,8 +73,8 @@ enum class ThemeColor {
TITLE -> appColors.title
SENT_MESSAGE -> appColors.sentMessage
RECEIVED_MESSAGE -> appColors.receivedMessage
WALLPAPER_BACKGROUND -> appColors.wallpaperBackground
WALLPAPER_TINT -> appColors.wallpaperTint
WALLPAPER_BACKGROUND -> appWallpaper.background
WALLPAPER_TINT -> appWallpaper.tint
}
}
@@ -102,8 +108,6 @@ 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) {
@@ -135,8 +139,6 @@ 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,
)
}
@@ -153,16 +155,79 @@ data class ThemeColors(
title = ac.title.toReadableHex(),
sentMessage = ac.sentMessage.toReadableHex(),
receivedMessage = ac.receivedMessage.toReadableHex(),
wallpaperBackground = ac.wallpaperBackground?.toReadableHex(),
wallpaperTint = ac.wallpaperTint?.toReadableHex(),
)
}
}
@Serializable
data class ThemeWallpaper (
val preset: String? = null,
val scale: Float = 1f,
val scaleType: BackgroundImageScaleType? = null,
val background: String? = null,
val tint: String? = null,
val image: String? = null,
) {
fun toAppWallpaper(): AppWallpaper {
return AppWallpaper(
background = background?.colorFromReadableHex(),
tint = tint?.colorFromReadableHex(),
type = if (preset != null) {
BackgroundImageType.Repeated(filename = preset, scale)
} else if (image != null) {
BackgroundImageType.Static(
filename = image,
scale,
scaleType ?: BackgroundImageScaleType.FILL
)
} else
null
)
}
fun withFilledWallpaper(): ThemeWallpaper {
val aw = toAppWallpaper()
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,
background = aw.background?.toReadableHex(),
tint = aw.tint?.toReadableHex(),
)
}
fun import(): ThemeWallpaper =
if (preset == null && image != null) {
// Need to save image from string and to save its path
try {
val parsed = base64ToBitmap(image)
val filename = saveBackgroundImage(parsed)
copy(image = filename)
} catch (e: Exception) {
ThemeWallpaper()
}
} else this
companion object {
fun from(type: BackgroundImageType, background: String?, tint: String?): ThemeWallpaper {
return ThemeWallpaper(
image = 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,
background = background,
tint = tint,
)
}
}
}
@Serializable
data class ThemeOverrides (
val base: DefaultTheme,
val colors: ThemeColors
val colors: ThemeColors,
val wallpaper: ThemeWallpaper = ThemeWallpaper(),
) {
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides {
return copy(colors = when (name) {
@@ -175,9 +240,14 @@ 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)
})
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
ThemeColor.WALLPAPER_TINT -> colors.copy()
}, wallpaper = when (name) {
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper.copy(background = color)
ThemeColor.WALLPAPER_TINT -> wallpaper.copy(tint = color)
else -> wallpaper.copy()
}
)
}
}
@@ -225,8 +295,6 @@ val DarkColorPaletteApp = AppColors(
title = SimplexBlue,
sentMessage = SentMessageColor,
receivedMessage = Color(0x20B1B0B5),
wallpaperBackground = null,
wallpaperTint = null,
)
val LightColorPalette = lightColors(
@@ -246,8 +314,6 @@ val LightColorPaletteApp = AppColors(
title = SimplexBlue,
sentMessage = SentMessageColor,
receivedMessage = Color(0x20B1B0B5),
wallpaperBackground = null,
wallpaperTint = null,
)
val SimplexColorPalette = darkColors(
@@ -268,8 +334,6 @@ val SimplexColorPaletteApp = AppColors(
title = Color(0xFF267BE5),
sentMessage = SentMessageColor,
receivedMessage = Color(0x20B1B0B5),
wallpaperBackground = null,
wallpaperTint = null,
)
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
@@ -8,6 +8,7 @@ import chat.simplex.res.MR
import chat.simplex.common.model.AppPreferences
import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.platform
import chat.simplex.common.views.helpers.BackgroundImageType
import chat.simplex.common.views.helpers.generalGetString
// https://github.com/rsms/inter
@@ -18,7 +19,7 @@ expect val EmojiFont: FontFamily
object ThemeManager {
private val appPrefs: AppPreferences = ChatController.appPrefs
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors)
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper)
private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) {
DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK
@@ -43,9 +44,9 @@ object ThemeManager {
else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp)
}
if (theme == null) {
return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third)
return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third, AppWallpaper())
}
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base))
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base), theme.wallpaper.toAppWallpaper())
}
fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides {
@@ -56,8 +57,8 @@ 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())
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base))
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper())
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base), wallpaper = nonFilledTheme.wallpaper.withFilledWallpaper())
}
// colors, default theme enum, localized name of theme
@@ -116,24 +117,38 @@ object ThemeManager {
if (colorToSet == null) {
// Setting default color from a base theme
colorToSet = when(nonSystemThemeName) {
DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp)
DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp)
DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp)
DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp, AppWallpaper())
DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp, AppWallpaper())
DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper())
// Will not be here
else -> return
}
}
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper())
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet?.toReadableHex())
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
fun saveAndApplyBackgroundImage(type: BackgroundImageType?, darkForSystemTheme: Boolean) {
val themeName = appPrefs.currentTheme.get()!!
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
themeName
} else {
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
}
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper())
overrides[nonSystemThemeName] = prevValue.copy(wallpaper = if (type != null) ThemeWallpaper.from(type, prevValue.wallpaper.background, prevValue.wallpaper.tint) else ThemeWallpaper())
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) {
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
overrides[theme.base.name] = prevValue.copy(colors = theme.colors)
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(), wallpaper = ThemeWallpaper())
overrides[theme.base.name] = prevValue.copy(colors = theme.colors, wallpaper = theme.wallpaper.import())
appPrefs.themeOverrides.set(overrides)
appPrefs.currentTheme.set(theme.base.name)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
@@ -148,7 +163,7 @@ object ThemeManager {
}
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[nonSystemThemeName] ?: return
overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors())
overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper.copy(background = null, tint = null))
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
@@ -597,8 +597,8 @@ fun ChatLayout(
drawerContentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { contentPadding ->
val backgroundImage = remember { chatModel.backgroundImage }.value
val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value
val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image
val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type
val defaultBackgroundColor = backgroundImageType?.defaultBackgroundColor
val defaultTintColor = backgroundImageType?.defaultTintColor
BoxWithConstraints(Modifier
@@ -9,23 +9,28 @@ 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.platform.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.File
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, "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));
enum class PredefinedBackgroundImage(val res: ImageResource, val filename: String, val scale: Float, val text: StringResource) {
@SerialName("cat") CAT(MR.images.background_cat, "simplex_cat", 0.5f, MR.strings.background_cat),
@SerialName("hearts") HEARTS(MR.images.background_hearts, "simplex_hearts", 0.5f, MR.strings.background_hearts),
@SerialName("school") SCHOOL(MR.images.background_school, "simplex_school", 0.5f, MR.strings.background_school),
@SerialName("internet") INTERNET(MR.images.background_internet, "simplex_internet", 0.5f, MR.strings.background_internet),
@SerialName("space") SPACE(MR.images.background_space, "simplex_space", 0.5f, MR.strings.background_space),
@SerialName("pets") PETS(MR.images.background_pets, "simplex_pets", 0.5f, MR.strings.background_pets),
@SerialName("rabbit") RABBIT(MR.images.background_rabbit, "simplex_rabbit", 0.5f, MR.strings.background_rabbit);
fun toType(): BackgroundImageType =
BackgroundImageType.Repeated(filename, scale)
companion object {
fun from(filename: String): PredefinedBackgroundImage? =
@@ -34,17 +39,31 @@ enum class PredefinedBackgroundImage(val res: ImageResource, val filename: Strin
}
@Serializable
enum class BackgroundImageScale(val contentScale: ContentScale, val text: StringResource) {
@SerialName("crop") CROP(ContentScale.Crop, MR.strings.background_image_scale_crop),
enum class BackgroundImageScaleType(val contentScale: ContentScale, val text: StringResource) {
@SerialName("fill") FILL(ContentScale.Crop, MR.strings.background_image_scale_fill),
@SerialName("fit") FIT(ContentScale.Fit, MR.strings.background_image_scale_fit),
@SerialName("fillWidth") FILL_WIDTH(ContentScale.FillWidth, MR.strings.background_image_scale_fill_width),
@SerialName("fillHeight") FILL_HEIGHT(ContentScale.FillHeight, MR.strings.background_image_scale_fill_height),
@SerialName("fillBounds") FILL_BOUNDS(ContentScale.FillBounds, MR.strings.background_image_scale_fill_bounds)
@SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.background_image_scale_repeat),
}
@Serializable
sealed class BackgroundImageType {
abstract val filename: String
val image by lazy {
val cache = cachedImage
if (cache != null && cache.first == filename) {
cache.second
} else {
val res = if (this is Repeated) {
PredefinedBackgroundImage.from(filename)!!.res.toComposeImageBitmap()!!
} else {
File(getBackgroundImageFilePath(filename)).inputStream().use { loadImageBitmap(it) }
}
cachedImage = filename to res
res
}
}
@Serializable @SerialName("repeated") data class Repeated(
override val filename: String,
val scale: Float,
@@ -52,34 +71,40 @@ sealed class BackgroundImageType {
@Serializable @SerialName("static") data class Static(
override val filename: String,
val scale: BackgroundImageScale,
val scale: Float,
val scaleType: BackgroundImageScaleType,
): BackgroundImageType()
val background: Color?
get() = CurrentColors.value.appColors.wallpaperBackground
get() = CurrentColors.value.wallpaper.background
val tint: Color?
get() = CurrentColors.value.appColors.wallpaperTint
fun toPredefined(): PredefinedBackgroundImage? = PredefinedBackgroundImage.from(filename)
get() = CurrentColors.value.wallpaper.tint
val defaultBackgroundColor: Color
@Composable get() = if (this is Static) MaterialTheme.colors.background else MaterialTheme.colors.background
@Composable get() = if (this is Static)
MaterialTheme.colors.background
else
MaterialTheme.colors.background
val defaultTintColor: Color
@Composable get() = if (this is Static) MaterialTheme.colors.background.copy(0.9f) else MaterialTheme.colors.primary
@Composable get() = if (this is Repeated || (this is Static && this.scaleType == BackgroundImageScaleType.REPEAT))
MaterialTheme.colors.primary
else
MaterialTheme.colors.background.copy(0.9f)
companion object {
val default: BackgroundImageType =
Repeated(PredefinedBackgroundImage.CAT.filename, 1f)
val default: BackgroundImageType
get() = PredefinedBackgroundImage.CAT.toType()
private var cachedImage: Pair<String, ImageBitmap>? = null
}
}
fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageType, defaultBackground: Color, defaultTint: Color) = clipRect {
drawRect(imageType.background ?: defaultBackground)
if (imageType is BackgroundImageType.Repeated) {
val scale = imageType.scale * density
fun repeat(imageScale: Float) {
val scale = imageScale * density
for (h in 0..(size.height / image.height / scale).roundToInt()) {
for (w in 0..(size.width / image.width / scale).roundToInt()) {
drawImage(
@@ -90,8 +115,15 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageT
)
}
}
}
drawRect(imageType.background ?: defaultBackground)
if (imageType is BackgroundImageType.Repeated) {
repeat(imageType.scale)
} else if (imageType is BackgroundImageType.Static && imageType.scaleType == BackgroundImageScaleType.REPEAT) {
repeat(imageType.scale)
} else if (imageType is BackgroundImageType.Static) {
val scale = imageType.scale.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
val scale = imageType.scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
val scaledWidth = (image.width * scale.scaleX).roundToInt()
val scaledHeight = (image.height * scale.scaleY).roundToInt()
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight))
@@ -160,34 +160,6 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
return null
}
fun getBackgroundImageFromUri(uri: URI, withAlertOnException: Boolean = true): Pair<String, ImageBitmap>? {
uri.inputStream().use {
runCatching {
return uri.toFile().name to loadImageBitmap(it!!)
}.onFailure {
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.import_background_image_error),
text = generalGetString(MR.strings.import_background_image_desc),
)
}
}
}
return null
}
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)
}
} else {
PredefinedBackgroundImage.from(type.filename)?.res?.toComposeImageBitmap()
}
return res ?: BackgroundImageType.default.toPredefined()!!.res.toComposeImageBitmap()!!
}
fun saveImage(uri: URI): CryptoFile? {
val bitmap = getBitmapFromUri(uri) ?: return null
return saveImage(bitmap)
@@ -312,9 +284,9 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile?
}
}
fun saveBackgroundImage(uri: URI): Pair<String, ImageBitmap>? {
val res = getBackgroundImageFromUri(uri, true) ?: return null
val destFile = File(getBackgroundImageFilePath(res.first))
fun saveBackgroundImage(uri: URI): String? {
val destFileName = generateNewFileName("background", "jpg", File(getBackgroundImageFilePath("")))
val destFile = File(getBackgroundImageFilePath(destFileName))
val inputStream = uri.inputStream()
try {
Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
@@ -322,9 +294,21 @@ fun saveBackgroundImage(uri: URI): Pair<String, ImageBitmap>? {
Log.e(TAG, "Error saving background image: ${e.stackTraceToString()}")
return null
}
return res
return destFile.name
}
fun saveBackgroundImage(image: ImageBitmap): String {
val destFileName = generateNewFileName("background", "jpg", File(getBackgroundImageFilePath("")))
val destFile = File(getBackgroundImageFilePath(destFileName))
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
val output = FileOutputStream(destFile)
dataResized.use {
it.writeTo(output)
}
return destFile.name
}
// TODO: removes all background for all themes. Should remove only unused
fun removeBackgroundImages(except: String? = null) {
File(getBackgroundImageFilePath("_")).parentFile.listFiles()?.forEach {
if (it.name != except) it.delete()
@@ -23,7 +23,6 @@ import androidx.compose.ui.unit.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
@@ -31,7 +30,6 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.chat.item.PreviewChatItemView
import chat.simplex.res.MR
import com.godaddy.android.colorpicker.*
import kotlinx.datetime.Clock
import kotlinx.serialization.encodeToString
import java.net.URI
@@ -106,8 +104,8 @@ object AppearanceScope {
) {
AppBarTitle(stringResource(MR.strings.choose_background_image_title))
val backgroundImage = remember { chatModel.backgroundImage }.value
val backgroundImageType = remember { appPrefs.backgroundImageType.state }.value
val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image
val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type
if (backgroundImage != null && backgroundImageType != null) {
ChatThemePreview(backgroundImage, backgroundImageType)
@@ -123,14 +121,13 @@ object AppearanceScope {
val imageTypeValues = remember {
PredefinedBackgroundImage.entries.map { it.filename to generalGetString(it.text) } + ("" to generalGetString(MR.strings.background_choose_own_image))
}
val systemDark = isSystemInDarkTheme()
val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) {
val res = saveBackgroundImage(to)
if (res != null) {
val (filename, backgroundImage) = res
val filename = saveBackgroundImage(to)
if (filename != null) {
imageTypeState.value = ""
chatModel.backgroundImage.value = backgroundImage
appPrefs.backgroundImageType.set(BackgroundImageType.Static(filename, BackgroundImageScale.CROP))
ThemeManager.saveAndApplyBackgroundImage(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL), systemDark)
removeBackgroundImages(filename)
resetColors()
}
@@ -145,8 +142,7 @@ object AppearanceScope {
withLongRunningApi { importBackgroundImageLauncher.launch("image/*") }
} else {
imageTypeState.value = filename
appPrefs.backgroundImageType.set(PredefinedBackgroundImage.from(filename)!!.type)
chatModel.backgroundImage.value = getBackgroundImageOrDefault()
ThemeManager.saveAndApplyBackgroundImage(PredefinedBackgroundImage.from(filename)!!.toType(), systemDark)
removeBackgroundImages()
}
}
@@ -159,23 +155,37 @@ object AppearanceScope {
state.value,
valueRange = 0.2f..2f,
onValueChange = {
appPrefs.backgroundImageType.set(backgroundImageType.copy(scale = it))
ThemeManager.saveAndApplyBackgroundImage(backgroundImageType.copy(scale = it), systemDark)
}
)
}
} else if (backgroundImageType is BackgroundImageType.Static) {
val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) }
val state = remember(backgroundImageType.scaleType) { mutableStateOf(backgroundImageType.scaleType) }
val values = remember {
BackgroundImageScale.entries.map { it to generalGetString(it.text) }
BackgroundImageScaleType.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))
onSelected = { scaleType ->
ThemeManager.saveAndApplyBackgroundImage(backgroundImageType.copy(scaleType = scaleType), systemDark)
}
)
if (backgroundImageType.scaleType == BackgroundImageScaleType.REPEAT) {
val state = remember(backgroundImageType.scale) { mutableStateOf(backgroundImageType.scale) }
Row {
Text("${state.value}", Modifier.width(50.dp))
Slider(
state.value,
valueRange = 0.2f..2f,
onValueChange = {
ThemeManager.saveAndApplyBackgroundImage(backgroundImageType.copy(scale = it), systemDark)
}
)
}
}
}
SectionSpacer()
@@ -245,12 +255,16 @@ object AppearanceScope {
editColor: (ThemeColor, Color) -> Unit
) {
val currentTheme by CurrentColors.collectAsState()
val systemDark = isSystemInDarkTheme()
SectionView(stringResource(MR.strings.settings_section_title_themes)) {
val selectedBackground = remember { appPrefs.backgroundImageType.state }.value
val selectedBackground = CurrentColors.collectAsState().value.wallpaper.type
val cornerRadius = remember { appPreferences.profileImageCornerRadius.state }
fun setBackground(type: BackgroundImageType?) {
appPrefs.backgroundImageType.set(type)
chatModel.backgroundImage.value = getBackgroundImageOrDefault()
if (type is BackgroundImageType.Static) {
ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_BACKGROUND, null, systemDark)
ThemeManager.saveAndApplyThemeColor(ThemeColor.WALLPAPER_TINT, null, systemDark)
}
ThemeManager.saveAndApplyBackgroundImage(type, systemDark)
removeBackgroundImages(type?.filename)
}
@Composable
@@ -272,12 +286,12 @@ object AppearanceScope {
Modifier
.size(width, height)
.background(MaterialTheme.colors.background).clip(RoundedCornerShape(percent = cornerRadius.value.roundToInt()))
.clickable { setBackground(background?.type) },
.clickable { setBackground(background?.toType()) },
contentAlignment = Alignment.Center
) {
if (background != null) {
val backgroundImage = remember(background.filename) { PredefinedBackgroundImage.from(background.filename)?.res?.toComposeImageBitmap() }
ChatThemePreview(backgroundImage, background.type, withMessages = false)
ChatThemePreview(backgroundImage, background.toType(), withMessages = false)
}
if ((background == null && selectedBackground == null) || (selectedBackground?.filename == background?.filename)) {
Checked()
@@ -289,10 +303,9 @@ object AppearanceScope {
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))
val filename = saveBackgroundImage(to)
if (filename != null) {
setBackground(BackgroundImageType.Static(filename, 1f, BackgroundImageScaleType.FILL))
}
}
}
@@ -305,7 +318,7 @@ object AppearanceScope {
},
contentAlignment = Alignment.Center
) {
val backgroundImage = remember { chatModel.backgroundImage }.value
val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image
if (type is BackgroundImageType.Static && backgroundImage != null) {
ChatThemePreview(backgroundImage, type, withMessages = false)
Checked()
@@ -322,7 +335,7 @@ object AppearanceScope {
BackgroundItem(background)
}
item {
OwnBackgroundItem(remember { appPrefs.backgroundImageType.state }.value)
OwnBackgroundItem(CurrentColors.collectAsState().value.wallpaper.type)
}
}
if (appPlatform.isDesktop) {
@@ -341,11 +354,11 @@ object AppearanceScope {
} else {
LazyHorizontalGrid(
rows = GridCells.Fixed(1),
Modifier.height(70.dp + DEFAULT_PADDING * 2),
Modifier.height(120.dp + DEFAULT_PADDING * 2),
contentPadding = PaddingValues(DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
) {
gridContent(50.dp, 70.dp)
gridContent(80.dp, 120.dp)
}
}
@@ -375,20 +388,20 @@ 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)
val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image
val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type
ChatThemePreview(backgroundImage, backgroundImageType)
SectionSpacer()
if (backgroundImageType != null) {
SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) {
val wallpaperBackgroundColor = currentTheme.appColors.wallpaperBackground ?: backgroundImageType.defaultBackgroundColor
val wallpaperBackgroundColor = currentTheme.wallpaper.background ?: 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
val wallpaperTintColor = currentTheme.wallpaper.tint ?: backgroundImageType.defaultTintColor
SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT, wallpaperTintColor) }) {
val title = generalGetString(MR.strings.color_wallpaper_tint)
Text(title)
@@ -446,7 +459,7 @@ object AppearanceScope {
}
}
val isInDarkTheme = isInDarkTheme()
if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) {
if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors, currentTheme.wallpaper)) {
SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) {
Text(generalGetString(MR.strings.reset_color), color = colors.primary)
}
@@ -500,9 +513,9 @@ object AppearanceScope {
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)
val backgroundImage = CurrentColors.collectAsState().value.wallpaper.type?.image
val backgroundImageType = CurrentColors.collectAsState().value.wallpaper.type
ChatThemePreview(backgroundImage, backgroundImageType)
SectionSpacer()
}
@@ -1551,11 +1551,9 @@
<string name="background_image_background_color">Background</string>
<string name="background_image_tint_color">Tint</string>
<string name="background_image_scale">Scale</string>
<string name="background_image_scale_crop">Crop</string>
<string name="background_image_scale_repeat">Repeat</string>
<string name="background_image_scale_fill">Fill</string>
<string name="background_image_scale_fit">Fit</string>
<string name="background_image_scale_fill_width">Fill width</string>
<string name="background_image_scale_fill_height">Fill height</string>
<string name="background_image_scale_fill_bounds">Fill bounds</string>
<!-- Preferences.kt -->
<string name="chat_preferences_you_allow">You allow</string>