changes
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
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()
|
||||
}
|
||||
}
|
||||
|
||||