mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 20:45:49 +00:00
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:
committed by
GitHub
parent
cfa7e0bb28
commit
2b7f3099a6
@@ -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 |
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -62,6 +62,9 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ProfileImageSection()
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
|
||||
SectionBottomSpacer()
|
||||
|
||||
Reference in New Issue
Block a user