This commit is contained in:
Avently
2024-04-30 18:04:20 +07:00
parent f1f853c18e
commit 1d5f0bcae2
23 changed files with 328 additions and 59 deletions
@@ -102,6 +102,7 @@ kotlin {
implementation("uk.co.caprica:vlcj:4.8.2")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
implementation("org.apache.xmlgraphics:batik-transcoder:1.16")
}
}
val desktopTest by getting
@@ -7,6 +7,8 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import android.text.BidiFormatter
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Font
@@ -14,9 +16,11 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.common.model.AppPreferences
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
@@ -51,3 +55,6 @@ actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
actual fun desktopExpandWindowToWidth(width: Dp) {}
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? =
getDrawable(androidAppContext)?.toBitmap()?.asImageBitmap()
@@ -152,7 +152,7 @@ fun AppearanceScope.AppearanceLayout(
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionDividerSpaced(maxTopPadding = true)
BackgroundImageSection()
BackgroundImageSection(showSettingsModal)
SectionBottomSpacer()
}
}
@@ -1908,12 +1908,13 @@ data class ChatItem (
itemDeleted: CIDeleted? = null,
itemEdited: Boolean = false,
itemTimed: CITimed? = null,
itemLive: Boolean = false,
deletable: Boolean = true,
editable: Boolean = true
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable),
meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem,
reactions = listOf(),
@@ -85,7 +85,7 @@ fun getBackgroundImageFilePath(fileName: String): String {
val path = if (rh == null) {
appearanceDir.absolutePath + s + fileName
} else {
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_appearance" + s + fileName
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName
}
File(path).parentFile.mkdirs()
return path
@@ -1,11 +1,13 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import com.russhwolf.settings.Settings
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@Composable
@@ -31,3 +33,5 @@ expect fun windowWidth(): Dp
expect fun desktopExpandWindowToWidth(width: Dp)
expect fun isRtl(text: CharSequence): Boolean
expect fun ImageResource.toComposeImageBitmap(): ImageBitmap?
@@ -5,14 +5,12 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.*
@@ -35,7 +33,6 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.newchat.ContactConnectionInfoView
import chat.simplex.res.MR
import kotlinx.coroutines.*
@@ -553,8 +550,6 @@ fun ChatLayout(
) {
val scope = rememberCoroutineScope()
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
val backgroundImage = remember { chatModel.backgroundImage }
val backgroundImageType = remember { appPrefs.backgroundImageType.state }
Box(
Modifier
.fillMaxWidth()
@@ -602,11 +597,14 @@ fun ChatLayout(
drawerContentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { contentPadding ->
val primaryColor = MaterialTheme.colors.primary
val backgroundImage = remember { chatModel.backgroundImage }
val backgroundImageType = remember { appPrefs.backgroundImageType.state }
val defaultBackgroundColor = backgroundImageType.value.defaultBackgroundColor
val defaultTintColor = backgroundImageType.value.defaultTintColor
BoxWithConstraints(Modifier
.fillMaxHeight()
.background(MaterialTheme.colors.background)
.drawBehind { chatViewBackground(backgroundImage.value, backgroundImageType.value, primaryColor) }
.drawBehind { chatViewBackground(backgroundImage.value, backgroundImageType.value, defaultBackgroundColor, defaultTintColor) }
.padding(contentPadding)
) {
ChatItemsList(
@@ -831,14 +831,14 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
@Preview
@Composable
fun PreviewChatItemView() {
fun PreviewChatItemView(
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
) {
SimpleXTheme {
ChatItemView(
rhId = null,
ChatInfo.Direct.sampleData,
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
chatItem,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
@@ -1,9 +1,11 @@
package chat.simplex.common.views.helpers
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
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
@@ -14,12 +16,17 @@ import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.math.max
import kotlin.math.roundToInt
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", 1f, null));
@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));
companion object {
fun from(filename: String): PredefinedBackgroundImage? =
@@ -28,25 +35,45 @@ enum class PredefinedBackgroundImage(val res: ImageResource, val filename: Strin
}
@Serializable
enum class BackgroundImageScale(val contentScale: ContentScale) {
@SerialName("crop") CROP(ContentScale.Crop),
@SerialName("fit") FIT(ContentScale.Fit),
@SerialName("fillWidth") FILL_WIDTH(ContentScale.FillWidth),
@SerialName("fillHeight") FILL_HEIGHT(ContentScale.FillHeight),
@SerialName("fillBounds") FILL_BOUNDS((ContentScale.FillBounds))
enum class BackgroundImageScale(val contentScale: ContentScale, val text: StringResource) {
@SerialName("crop") CROP(ContentScale.Crop, MR.strings.background_image_scale_crop),
@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)
}
@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 tint: String?): BackgroundImageType()
@Serializable @SerialName("static") data class Static(override val custom: Boolean = true, override val filename: String, val scale: BackgroundImageScale, val tint: String?): BackgroundImageType()
@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()
val tintColor: Color? by lazy {
@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 -> tint?.colorFromReadableHex()
is Static -> tint?.colorFromReadableHex()
is Repeated -> backgroundColor?.colorFromReadableHex()
is Static -> backgroundColor?.colorFromReadableHex()
}
}
val tint: Color? by lazy {
when (this) {
is Repeated -> tintColor?.colorFromReadableHex()
is Static -> tintColor?.colorFromReadableHex()
}
}
@@ -56,22 +83,42 @@ sealed class BackgroundImageType {
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())
}
val defaultBackgroundColor: Color
@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
companion object {
val default: BackgroundImageType =
Repeated(custom = false, PredefinedBackgroundImage.CAT.filename, 1f, null)
Repeated(custom = false, PredefinedBackgroundImage.CAT.filename, 1f)
}
}
fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageType, defaultTint: Color) {
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
val scale = imageType.scale * density
for (h in 0..(size.height / image.height / scale).roundToInt()) {
for (w in 0..(size.width / image.width / scale).roundToInt()) {
drawImage(
image,
dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()),
dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()),
colorFilter = ColorFilter.tint(imageType.tintColor ?: defaultTint)
colorFilter = ColorFilter.tint(imageType.tint ?: defaultTint)
)
}
}
@@ -79,6 +126,7 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageT
val scale = imageType.scale.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 = (max(0f, size.width - scaledWidth) / 2).roundToInt(), y = (max(0f, size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight))
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight))
drawRect(imageType.tint ?: defaultTint)
}
}
}
@@ -14,12 +14,15 @@ import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
import chat.simplex.res.MR
import com.charleskorn.kaml.decodeFromStream
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.decodeFromStream
import java.io.*
import java.net.URI
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
@@ -180,9 +183,9 @@ fun getBackgroundImageOrDefault(): ImageBitmap {
loadImageBitmap(it)
}
} else {
PredefinedBackgroundImage.from(type.filename)?.res?.image?.toComposeImageBitmap()
PredefinedBackgroundImage.from(type.filename)?.res?.toComposeImageBitmap()
}
return res ?: BackgroundImageType.default.toPredefined()!!.res.image.toComposeImageBitmap()
return res ?: BackgroundImageType.default.toPredefined()!!.res.toComposeImageBitmap()!!
}
fun saveImage(uri: URI): CryptoFile? {
@@ -314,7 +317,7 @@ fun saveBackgroundImage(uri: URI): Pair<String, ImageBitmap>? {
val destFile = File(getBackgroundImageFilePath(res.first))
val inputStream = uri.inputStream()
try {
Files.copy(inputStream!!, destFile.toPath())
Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
} catch (e: Exception) {
Log.e(TAG, "Error saving background image: ${e.stackTraceToString()}")
return null
@@ -322,8 +325,10 @@ fun saveBackgroundImage(uri: URI): Pair<String, ImageBitmap>? {
return res
}
fun removeBackgroundImage(fileName: String) {
File(getBackgroundImageFilePath(fileName)).delete()
fun removeBackgroundImages(except: String? = null) {
File(getBackgroundImageFilePath("_")).parentFile.listFiles()?.forEach {
if (it.name != except) it.delete()
}
}
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
@@ -5,19 +5,21 @@ import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionView
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
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.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
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.*
@@ -25,12 +27,12 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.chat.SendReceipts
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
import java.nio.file.Files
import java.util.*
import kotlin.collections.ArrayList
@@ -92,14 +94,42 @@ object AppearanceScope {
}
@Composable
fun BackgroundImageSection() {
fun BackgroundImageSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
) {
SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) {
val pref = remember { appPrefs.backgroundImageType.state }
val state = remember {
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 values = remember {
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? ->
@@ -107,30 +137,122 @@ object AppearanceScope {
val res = saveBackgroundImage(to)
if (res != null) {
val (filename, backgroundImage) = res
state.value = ""
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),
values,
state,
enabled = remember { mutableStateOf(true) },
imageTypeValues,
imageTypeState,
onSelected = { filename ->
if (filename.isEmpty()) {
withLongRunningApi { importBackgroundImageLauncher.launch("image/*") }
} else {
if (state.value.isEmpty()) {
removeBackgroundImage(appPrefs.backgroundImageType.get().filename)
}
state.value = filename
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()
}
}
@@ -712,6 +712,7 @@
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string>
<string name="appearance_settings">Appearance</string>
<string name="customize_theme_title">Customize theme</string>
<string name="choose_background_image_title">Choose background</string>
<string name="theme_colors_section_title">THEME COLORS</string>
<string name="app_version_title">App version</string>
<string name="app_version_name">App version: v%s</string>
@@ -1535,8 +1536,24 @@
<string name="color_received_message">Received message</string>
<!-- Backgrounds -->
<string name="background_choose_own_image">Choose you own</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>
<string name="background_internet">Internet</string>
<string name="background_space">Space</string>
<string name="background_pets">Pets</string>
<string name="background_rabbit">Rabbit</string>
<string name="background_image_preview_hello_alice">Hello, Alice</string>
<string name="background_image_preview_hello_bob">Hello, Bob</string>
<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_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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

@@ -0,0 +1,59 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
package chat.simplex.common.other.mokoresources
import chat.simplex.res.MR.assets.resourcesClassLoader
import dev.icerock.moko.resources.ImageResource
import org.apache.batik.transcoder.*
import org.apache.batik.transcoder.image.PNGTranscoder
import java.awt.image.BufferedImage
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import javax.imageio.ImageIO
private var cache: Pair<String, BufferedImage>? = null
// Get rid of this file when we update to moko-resources >= 0.24.0 and use `image` instead of `customImage()`
// See https://github.com/icerockdev/moko-resources/commit/93900ca2690d2c70cf4db24902a4b89f30877176
fun ImageResource.customImage(): BufferedImage {
if (cache?.first == filePath) return cache!!.second
val stream = resourcesClassLoader.getResourceAsStream(filePath)
?: throw FileNotFoundException("Couldn't open resource as stream at: $filePath")
val res = stream.use {
if (filePath.endsWith(".svg", ignoreCase = true)) {
readSvg(it)
} else {
ImageIO.read(it)
}
}
cache = filePath to res
return res
}
private fun readSvg(
inputStream: InputStream
): BufferedImage {
// Create a PNG transcoder.
val t: Transcoder = PNGTranscoder()
// Create the transcoder input.
val input = TranscoderInput(inputStream)
// Create the transcoder output.
val tempFile: File = File.createTempFile("moko-resources", ".png")
try {
tempFile.outputStream().use {
val output = TranscoderOutput(it)
t.transcode(input, output)
}
return tempFile.inputStream().use {
ImageIO.read(it)
}
} finally {
tempFile.delete()
}
}
@@ -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_appearance")
actual val appearanceDir: 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"
@@ -1,12 +1,16 @@
package chat.simplex.common.platform
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.common.other.mokoresources.customImage
import chat.simplex.common.simplexWindowState
import com.russhwolf.settings.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
import java.io.File
@@ -58,3 +62,6 @@ actual fun isRtl(text: CharSequence): Boolean {
dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
}
}
actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? =
customImage().toComposeImageBitmap()
@@ -69,7 +69,7 @@ fun AppearanceScope.AppearanceLayout(
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionDividerSpaced(maxTopPadding = true)
BackgroundImageSection()
BackgroundImageSection(showSettingsModal)
SectionBottomSpacer()
}
}