diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 42e4ac2591..5cc19d62fc 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -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 diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt index e15d1f9268..91d19759ea 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -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() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 256e38e009..3b95bb9fda 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -152,7 +152,7 @@ fun AppearanceScope.AppearanceLayout( ThemesSection(systemDarkTheme, showSettingsModal, editColor) SectionDividerSpaced(maxTopPadding = true) - BackgroundImageSection() + BackgroundImageSection(showSettingsModal) SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 73a8574231..d5c9c20490 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -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(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 57f268f064..09dee94576 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt index 2ee668fb23..8e45ded4f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt @@ -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? diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index b3d3b09ee5..a6c7d602fb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 10078dc266..e2adfedf8c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -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)) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt index f2f2bd573c..cf457b2934 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatViewBackground.kt @@ -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) } -} \ No newline at end of file +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index c69bbf63a6..3a6b0fdd49 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -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? { 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? { 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 createTmpFileAndDelete(onCreated: (File) -> T): T { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index 2162c90f1d..c0de6c2a1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -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() } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index cdc79e2759..e8829e84c9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -712,6 +712,7 @@ Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]> Appearance Customize theme + Choose background THEME COLORS App version App version: v%s @@ -1535,8 +1536,24 @@ Received message - Choose you own… + Choose… Cat + Hearts + School + Internet + Space + Pets + Rabbit + Hello, Alice + Hello, Bob + Background + Tint + Scale + Crop + Fit + Fill width + Fill height + Fill bounds You allow diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_cat@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_cat@4x.png index 5643b00e0e..f2691bbdbc 100644 Binary files a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_cat@4x.png and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_cat@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_hearts@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_hearts@4x.png new file mode 100644 index 0000000000..58814044ff Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_hearts@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_internet@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_internet@4x.png new file mode 100644 index 0000000000..dea6700d4a Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_internet@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_pets@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_pets@4x.png new file mode 100644 index 0000000000..dcd01bb7b7 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_pets@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_rabbit@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_rabbit@4x.png new file mode 100644 index 0000000000..0eaa3e297c Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_rabbit@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_school@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_school@4x.png new file mode 100644 index 0000000000..5a14e596ce Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_school@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/background_space@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_space@4x.png new file mode 100644 index 0000000000..17632573d6 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/background_space@4x.png differ diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/other/mokoresources/ImageReader.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/other/mokoresources/ImageReader.kt new file mode 100644 index 0000000000..a978886914 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/other/mokoresources/ImageReader.kt @@ -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? = 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() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 43fd42b396..0ac6d1f048 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -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" diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index b758988227..f9d3902b6e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -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() \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index e21ffe24e7..cad1575a05 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -69,7 +69,7 @@ fun AppearanceScope.AppearanceLayout( ThemesSection(systemDarkTheme, showSettingsModal, editColor) SectionDividerSpaced(maxTopPadding = true) - BackgroundImageSection() + BackgroundImageSection(showSettingsModal) SectionBottomSpacer() } }