android, desktop: wallpapers

This commit is contained in:
Avently
2024-04-30 01:28:20 +07:00
parent d5a0c5e56a
commit f1f853c18e
15 changed files with 241 additions and 10 deletions

View File

@@ -15,6 +15,7 @@ actual val dataDir: File = androidAppContext.dataDir
actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE)
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files")
actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files")
actual val appearanceDir: File = File(filesDir.absolutePath + File.separator + "appearance")
actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files")
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files"

View File

@@ -150,6 +150,9 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionDividerSpaced(maxTopPadding = true)
BackgroundImageSection()
SectionBottomSpacer()
}
}

View File

@@ -4,7 +4,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
@@ -137,6 +137,8 @@ object ChatModel {
val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000)
val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = ProcessedErrors(20_000)
val backgroundImage by lazy { mutableStateOf(getBackgroundImageOrDefault()) }
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
} else {

View File

@@ -1,8 +1,10 @@
package chat.simplex.common.model
import androidx.compose.material.MaterialTheme
import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.setNetCfg
@@ -11,6 +13,7 @@ import chat.simplex.common.model.ChatModel.changingActiveUserMutex
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.call.*
import chat.simplex.common.views.migration.MigrationFileLinkData
import chat.simplex.common.views.onboarding.OnboardingStage
@@ -172,6 +175,18 @@ class AppPreferences {
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, settingsThemes)
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
private val _backgroundImageType = mkStrPreference(SHARED_PREFS_BACKGROUND_IMAGE, json.encodeToString(BackgroundImageType.default))
val backgroundImageType: SharedPreference<BackgroundImageType> = SharedPreference(
get = fun(): BackgroundImageType {
val value = _backgroundImageType.get() ?: return BackgroundImageType.default
return try {
json.decodeFromString(value)
} catch (e: Throwable) {
BackgroundImageType.default
}
},
set = fun(type: BackgroundImageType) { _backgroundImageType.set(json.encodeToString(type)) }
)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@@ -339,6 +354,7 @@ class AppPreferences {
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
private const val SHARED_PREFS_THEMES = "Themes"
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
private const val SHARED_PREFS_BACKGROUND_IMAGE = "BackgroundImage"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"

View File

@@ -14,6 +14,7 @@ expect val dataDir: File
expect val tmpDir: File
expect val filesDir: File
expect val appFilesDir: File
expect val appearanceDir: File
expect val coreTmpDir: File
expect val dbAbsolutePrefixPath: String
@@ -78,6 +79,18 @@ fun getAppFilePath(fileName: String): String {
}
}
fun getBackgroundImageFilePath(fileName: String): String {
val rh = chatModel.currentRemoteHost.value
val s = File.separator
val path = if (rh == null) {
appearanceDir.absolutePath + s + fileName
} else {
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_appearance" + s + fileName
}
File(path).parentFile.mkdirs()
return path
}
fun getLoadedFilePath(file: CIFile?): String? {
val f = file?.fileSource?.filePath
return if (f != null && file.loaded) {

View File

@@ -10,6 +10,8 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.isInNightMode
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
@@ -145,11 +147,6 @@ data class ThemeColors(
}
}
private fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
@Serializable
data class ThemeOverrides (
val base: DefaultTheme,

View File

@@ -152,6 +152,9 @@ object ThemeManager {
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
}
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
}

View File

@@ -13,17 +13,18 @@ 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.graphics.Color
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
@@ -34,6 +35,7 @@ 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.*
@@ -551,6 +553,8 @@ 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()
@@ -596,9 +600,13 @@ fun ChatLayout(
floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { contentPadding ->
val primaryColor = MaterialTheme.colors.primary
BoxWithConstraints(Modifier
.fillMaxHeight()
.background(MaterialTheme.colors.background)
.drawBehind { chatViewBackground(backgroundImage.value, backgroundImageType.value, primaryColor) }
.padding(contentPadding)
) {
ChatItemsList(

View File

@@ -0,0 +1,84 @@
package chat.simplex.common.views.helpers
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import chat.simplex.res.MR
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.math.max
import kotlin.math.roundToInt
@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));
companion object {
fun from(filename: String): PredefinedBackgroundImage? =
entries.firstOrNull { it.filename == filename }
}
}
@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))
}
@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()
val tintColor: Color? by lazy {
when (this) {
is Repeated -> tint?.colorFromReadableHex()
is Static -> tint?.colorFromReadableHex()
}
}
fun toPredefined(): PredefinedBackgroundImage? =
when (this) {
is Repeated -> if (!custom) PredefinedBackgroundImage.from(filename) else null
is Static -> if (!custom) PredefinedBackgroundImage.from(filename) else null
}
companion object {
val default: BackgroundImageType =
Repeated(custom = false, PredefinedBackgroundImage.CAT.filename, 1f, null)
}
}
fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: BackgroundImageType, defaultTint: Color) {
if (imageType is BackgroundImageType.Repeated) {
val scale = imageType.scale
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)
)
}
}
} else if (imageType is BackgroundImageType.Static) {
val scale = imageType.scale.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
val 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))
}
}

View File

@@ -16,6 +16,7 @@ import com.charleskorn.kaml.decodeFromStream
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.decodeFromStream
import java.io.*
import java.net.URI
import java.nio.file.Files
@@ -156,6 +157,34 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
return null
}
fun getBackgroundImageFromUri(uri: URI, withAlertOnException: Boolean = true): Pair<String, ImageBitmap>? {
uri.inputStream().use {
runCatching {
return uri.toFile().name to loadImageBitmap(it!!)
}.onFailure {
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.import_background_image_error),
text = generalGetString(MR.strings.import_background_image_desc),
)
}
}
}
return null
}
fun getBackgroundImageOrDefault(): ImageBitmap {
val type = appPreferences.backgroundImageType.get()
val res = if (type.custom) {
File(getBackgroundImageFilePath(type.filename)).inputStream().use {
loadImageBitmap(it)
}
} else {
PredefinedBackgroundImage.from(type.filename)?.res?.image?.toComposeImageBitmap()
}
return res ?: BackgroundImageType.default.toPredefined()!!.res.image.toComposeImageBitmap()
}
fun saveImage(uri: URI): CryptoFile? {
val bitmap = getBitmapFromUri(uri) ?: return null
return saveImage(bitmap)
@@ -280,6 +309,23 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile?
}
}
fun saveBackgroundImage(uri: URI): Pair<String, ImageBitmap>? {
val res = getBackgroundImageFromUri(uri, true) ?: return null
val destFile = File(getBackgroundImageFilePath(res.first))
val inputStream = uri.inputStream()
try {
Files.copy(inputStream!!, destFile.toPath())
} catch (e: Exception) {
Log.e(TAG, "Error saving background image: ${e.stackTraceToString()}")
return null
}
return res
}
fun removeBackgroundImage(fileName: String) {
File(getBackgroundImageFilePath(fileName)).delete()
}
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
tmpFile.deleteOnExit()

View File

@@ -19,14 +19,18 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.chat.SendReceipts
import chat.simplex.res.MR
import com.godaddy.android.colorpicker.*
import kotlinx.serialization.encodeToString
import java.net.URI
import java.nio.file.Files
import java.util.*
import kotlin.collections.ArrayList
@@ -87,6 +91,49 @@ object AppearanceScope {
SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) }
}
@Composable
fun BackgroundImageSection() {
SectionView(stringResource(MR.strings.settings_section_title_background_image).uppercase()) {
val pref = remember { appPrefs.backgroundImageType.state }
val state = remember {
val type = appPrefs.backgroundImageType.get()
mutableStateOf(if (type.custom) "" else type.filename)
}
val values = remember {
PredefinedBackgroundImage.entries.map { it.filename to generalGetString(it.text) } + ("" to generalGetString(MR.strings.background_choose_own_image))
}
val importBackgroundImageLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) {
val res = saveBackgroundImage(to)
if (res != null) {
val (filename, backgroundImage) = res
state.value = ""
chatModel.backgroundImage.value = backgroundImage
appPrefs.backgroundImageType.set(BackgroundImageType.Static(custom = true, filename, BackgroundImageScale.CROP, null))
}
}
}
ExposedDropDownSettingRow(
stringResource(MR.strings.settings_section_title_background_image),
values,
state,
enabled = remember { mutableStateOf(true) },
onSelected = { filename ->
if (filename.isEmpty()) {
withLongRunningApi { importBackgroundImageLauncher.launch("image/*") }
} else {
if (state.value.isEmpty()) {
removeBackgroundImage(appPrefs.backgroundImageType.get().filename)
}
state.value = filename
appPrefs.backgroundImageType.set(PredefinedBackgroundImage.from(filename)!!.type)
chatModel.backgroundImage.value = getBackgroundImageOrDefault()
}
}
)
}
}
@Composable
fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) {
ColumnWithScrollBar(

View File

@@ -1034,6 +1034,7 @@
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_profile_images">Profile images</string>
<string name="settings_section_title_background_image">Background image</string>
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_network_connection">Network connection</string>
@@ -1519,6 +1520,8 @@
<string name="import_theme">Import theme</string>
<string name="import_theme_error">Import theme error</string>
<string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string>
<string name="import_background_image_error">Import background image error</string>
<string name="import_background_image_desc">Make sure the file can be read correctly.</string>
<string name="export_theme">Export theme</string>
<string name="reset_color">Reset colors</string>
<string name="color_primary">Accent</string>
@@ -1531,6 +1534,10 @@
<string name="color_sent_message">Sent message</string>
<string name="color_received_message">Received message</string>
<!-- Backgrounds -->
<string name="background_choose_own_image">Choose you own…</string>
<string name="background_cat">Cat</string>
<!-- Preferences.kt -->
<string name="chat_preferences_you_allow">You allow</string>
<string name="chat_preferences_contact_allows">Contact allows</string>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -13,6 +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 coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp")
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1"

View File

@@ -67,6 +67,9 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionDividerSpaced(maxTopPadding = true)
BackgroundImageSection()
SectionBottomSpacer()
}
}