android, desktop: customizable profile images (#4087)

* android, desktop: customizable profile images

* better

* fixes

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-04-25 03:22:52 +07:00
committed by GitHub
parent cfa7e0bb28
commit 2b7f3099a6
14 changed files with 118 additions and 22 deletions

View File

@@ -12,10 +12,12 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
@@ -34,11 +36,13 @@ import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
enum class AppIcon(val resId: Int) {
DEFAULT(R.drawable.icon_round_common),
DARK_BLUE(R.drawable.icon_dark_blue_round_common),
enum class AppIcon(val image: ImageResource) {
DEFAULT(MR.images.ic_simplex_light),
DARK_BLUE(MR.images.ic_simplex_dark),
}
@Composable
@@ -122,9 +126,8 @@ fun AppearanceScope.AppearanceLayout(
LazyRow {
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
val item = AppIcon.values()[index]
val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!!
Image(
bitmap = mipmap.toBitmap().asImageBitmap(),
painterResource(item.image),
contentDescription = "",
contentScale = ContentScale.Fit,
modifier = Modifier
@@ -132,6 +135,7 @@ fun AppearanceScope.AppearanceLayout(
.size(70.dp)
.clickable { changeIcon(item) }
.padding(10.dp)
.clip(CircleShape)
)
if (index + 1 != AppIcon.values().size) {
@@ -141,6 +145,9 @@ fun AppearanceScope.AppearanceLayout(
}
}
SectionDividerSpaced(maxTopPadding = true)
ProfileImageSection()
SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionBottomSpacer()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -171,6 +171,7 @@ class AppPreferences {
}, decode = {
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, settingsThemes)
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@@ -201,6 +202,12 @@ class AppPreferences {
set = fun(value) = settings.putLong(prefName, value)
)
private fun mkFloatPreference(prefName: String, default: Float) =
SharedPreference(
get = fun() = settings.getFloat(prefName, default),
set = fun(value) = settings.putFloat(prefName, value)
)
private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference<Long> {
val d = if (networkUseSocksProxy.get()) proxyDefault else default
return SharedPreference(
@@ -331,6 +338,7 @@ class AppPreferences {
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
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_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

@@ -1010,13 +1010,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
swipeableModifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(
Modifier
.clip(CircleShape)
.clickable {
showMemberInfo(chat.chatInfo.groupInfo, member)
}
) {
Box(Modifier.clickable { showMemberInfo(chat.chatInfo.groupInfo, member) }) {
MemberImage(member)
}
ChatItemViewShortHand(cItem, range)

View File

@@ -3,22 +3,24 @@ package chat.simplex.common.views.helpers
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.unit.*
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.dp
import chat.simplex.common.model.ChatInfo
import chat.simplex.common.platform.appPreferences
import chat.simplex.common.platform.base64ToBitmap
import chat.simplex.common.ui.theme.NoteFolderIconColor
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -79,12 +81,32 @@ fun ProfileImage(
imageBitmap,
stringResource(MR.strings.image_descr_profile_image),
contentScale = ContentScale.Crop,
modifier = Modifier.size(size).padding(size / 12).clip(CircleShape)
modifier = Modifier.size(size).padding(size / 12).clip(ProfileIconShape())
)
}
}
}
@Composable
fun ProfileImage(size: Dp, image: ImageResource) {
Image(
painterResource(image),
stringResource(MR.strings.image_descr_profile_image),
contentScale = ContentScale.Crop,
modifier = Modifier.size(size).padding(size / 12).clip(ProfileIconShape())
)
}
@Composable
fun ProfileIconShape(): Shape {
val percent = remember { appPreferences.profileImageCornerRadius.state }
return when {
percent.value <= 0 -> RectangleShape
percent.value >= 50 -> CircleShape
else -> RoundedCornerShape(PercentCornerSize(percent.value))
}
}
/** [AccountCircleFilled] has its inner padding which leads to visible border if there is background underneath.
* This is workaround
* */
@@ -109,11 +131,30 @@ fun ProfileImageForActiveCall(
imageBitmap,
stringResource(MR.strings.image_descr_profile_image),
contentScale = ContentScale.Crop,
modifier = Modifier.size(size).clip(CircleShape)
modifier = Modifier.size(size).clip(ProfileIconShape())
)
}
}
/** (c) [androidx.compose.foundation.shape.CornerSize] */
private data class PercentCornerSize(
private val percent: Float
) : CornerSize, InspectableValue {
init {
if (percent < 0 || percent > 100) {
throw IllegalArgumentException("The percent should be in the range of [0, 100]")
}
}
override fun toPx(shapeSize: Size, density: Density) =
shapeSize.minDimension * (percent / 100f)
override fun toString(): String = "CornerSize(size = $percent%)"
override val valueOverride: String
get() = "$percent%"
}
@Preview
@Composable

View File

@@ -539,6 +539,11 @@ private val versionDescriptions: List<VersionDescription> = listOf(
titleId = MR.strings.v5_7_call_sounds,
descrId = MR.strings.v5_7_call_sounds_descr
),
FeatureDescription(
icon = MR.images.ic_account_box,
titleId = MR.strings.v5_7_shape_profile_images,
descrId = MR.strings.v5_7_shape_profile_images_descr
),
FeatureDescription(
icon = MR.images.ic_wifi_tethering,
titleId = MR.strings.v5_7_network,

View File

@@ -5,14 +5,16 @@ import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
@@ -24,7 +26,6 @@ import chat.simplex.common.platform.*
import chat.simplex.res.MR
import com.godaddy.android.colorpicker.*
import kotlinx.serialization.encodeToString
import java.io.File
import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
@@ -33,6 +34,37 @@ import kotlin.collections.ArrayList
expect fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit))
object AppearanceScope {
@Composable
fun ProfileImageSection() {
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
val image = remember { chatModel.currentUser }.value?.image
Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
val size = 60
Box(Modifier.offset(x = -(size / 12).dp)) {
if (!image.isNullOrEmpty()) {
ProfileImage(size.dp, image, MR.images.ic_simplex_light, color = Color.Unspecified)
} else {
ProfileImage(size.dp, if (isInDarkTheme()) MR.images.ic_simplex_light else MR.images.ic_simplex_dark)
}
}
Spacer(Modifier.width(DEFAULT_PADDING_HALF - (size / 12).dp))
Slider(
remember { appPreferences.profileImageCornerRadius.state }.value,
valueRange = 0f..50f,
steps = 20,
onValueChange = {
val diff = it % 2.5f
appPreferences.profileImageCornerRadius.set(it + (if (diff >= 1.25f) -diff + 2.5f else -diff))
},
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
}
}
}
@Composable
fun ThemesSection(
systemDarkTheme: SharedPreference<String?>,
@@ -41,7 +73,7 @@ object AppearanceScope {
) {
val currentTheme by CurrentColors.collectAsState()
SectionView(stringResource(MR.strings.settings_section_title_themes)) {
val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme()
val darkTheme = isSystemInDarkTheme()
val state = remember { derivedStateOf { currentTheme.name } }
ThemeSelector(state) {
ThemeManager.applyTheme(it, darkTheme)

View File

@@ -1033,6 +1033,7 @@
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
<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_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_network_connection">Network connection</string>
@@ -1770,6 +1771,8 @@
<string name="v5_7_forward_descr">Message source remains private.</string>
<string name="v5_7_call_sounds">In-call sounds</string>
<string name="v5_7_call_sounds_descr">When connecting audio and video calls.</string>
<string name="v5_7_shape_profile_images">Shape profile images</string>
<string name="v5_7_shape_profile_images_descr">Square, circle, or anything in between.</string>
<string name="v5_7_network">Network management</string>
<string name="v5_7_network_descr">More reliable network connection.</string>
<string name="v5_7_new_interface_languages">Lithuanian UI</string>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M182-218q59.315-55.57 134.804-89.785Q392.293-342 479.896-342q87.604 0 163.197 34.215Q718.685-273.57 778-218v-560H182v560Zm300.232-200.5q57.268 0 96.518-39.482Q618-497.465 618-554.732q0-57.268-39.482-96.518-39.483-39.25-96.75-39.25-57.268 0-96.518 39.482Q346-611.535 346-554.268q0 57.268 39.482 96.518 39.483 39.25 96.75 39.25ZM182-124.5q-22.969 0-40.234-17.266Q124.5-159.031 124.5-182v-596q0-22.969 17.266-40.234Q159.031-835.5 182-835.5h596q22.969 0 40.234 17.266Q835.5-800.969 835.5-778v596q0 22.969-17.266 40.234Q800.969-124.5 778-124.5H182Zm52.5-57.5h491v-9.111Q671.5-237.5 609.161-261 546.823-284.5 480-284.5q-67.177 0-129.339 23.5Q288.5-237.5 234.5-191.111V-182Zm247.441-294q-32.733 0-55.587-22.913-22.854-22.913-22.854-55.646 0-32.733 22.913-55.587Q449.326-633 482.059-633q32.733 0 55.587 22.913 22.854 22.913 22.854 55.646 0 32.733-22.913 55.587Q514.674-476 481.941-476ZM480-498.5Z"/></svg>

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1848,5 +1848,7 @@
<string name="v5_7_new_interface_languages">Литовский интерфейс</string>
<string name="v5_7_forward_descr">Источник сообщения остаётся конфиденциальным.</string>
<string name="v5_7_call_sounds_descr">Во время соединения аудио и видео звонков.</string>
<string name="v5_7_shape_profile_images">Форма картинок профилей</string>
<string name="v5_7_shape_profile_images_descr">Квадрат, круг и все, что между ними.</string>
<string name="v5_7_quantum_resistant_encryption_descr">Будет включено в прямых разговорах!</string>
</resources>

View File

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