mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-12 21:45:55 +00:00
more color options and better code
This commit is contained in:
@@ -119,6 +119,7 @@ dependencies {
|
||||
implementation 'androidx.fragment:fragment:1.4.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
implementation 'com.charleskorn.kaml:kaml:0.43.0'
|
||||
//implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-util:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||
|
||||
@@ -25,6 +25,8 @@ import chat.simplex.app.views.call.*
|
||||
import chat.simplex.app.views.newchat.ConnectViaLinkTab
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import com.charleskorn.kaml.Yaml
|
||||
import com.charleskorn.kaml.YamlConfiguration
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
@@ -2991,6 +2993,11 @@ val json = Json {
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
val yaml = Yaml(configuration = YamlConfiguration(
|
||||
strictMode = false,
|
||||
encodeDefaults = false,
|
||||
))
|
||||
|
||||
@Serializable
|
||||
class APIResponse(val resp: CR, val corr: String? = null) {
|
||||
companion object {
|
||||
|
||||
@@ -10,34 +10,84 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
enum class DefaultTheme {
|
||||
SYSTEM, LIGHT, DARK, BLUE;
|
||||
|
||||
// Call in only with base theme, not SYSTEM
|
||||
fun hasChangedPrimary(colors: Colors): Boolean {
|
||||
return when (this) {
|
||||
// Call it only with base theme, not SYSTEM
|
||||
fun hasChangedAnyColor(colors: Colors): Boolean {
|
||||
val palette = when (this) {
|
||||
SYSTEM -> return false
|
||||
LIGHT -> colors.primary != LightColorPalette.primary
|
||||
DARK -> colors.primary != DarkColorPalette.primary
|
||||
BLUE -> colors.primary != BlueColorPalette.primary
|
||||
LIGHT -> LightColorPalette
|
||||
DARK -> DarkColorPalette
|
||||
BLUE -> BlueColorPalette
|
||||
}
|
||||
return with(palette) {
|
||||
colors.primary != primary ||
|
||||
colors.primaryVariant != primaryVariant ||
|
||||
colors.secondary != secondary ||
|
||||
colors.secondaryVariant != secondaryVariant ||
|
||||
colors.background != background ||
|
||||
colors.surface != surface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ThemeColor {
|
||||
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE;
|
||||
|
||||
fun fromColors(colors: Colors): Color {
|
||||
return when (this) {
|
||||
PRIMARY -> colors.primary
|
||||
PRIMARY_VARIANT -> colors.primaryVariant
|
||||
SECONDARY -> colors.secondary
|
||||
SECONDARY_VARIANT -> colors.secondaryVariant
|
||||
BACKGROUND -> colors.background
|
||||
SURFACE -> colors.surface
|
||||
}
|
||||
}
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
PRIMARY -> generalGetString(R.string.color_primary)
|
||||
PRIMARY_VARIANT -> generalGetString(R.string.color_primary_variant)
|
||||
SECONDARY -> generalGetString(R.string.color_secondary)
|
||||
SECONDARY_VARIANT -> generalGetString(R.string.color_secondary_variant)
|
||||
BACKGROUND -> generalGetString(R.string.color_background)
|
||||
SURFACE -> generalGetString(R.string.color_surface)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemeColors(
|
||||
val primary: String? = null
|
||||
val primary: String? = null,
|
||||
val primaryVariant: String? = null,
|
||||
val secondary: String? = null,
|
||||
val secondaryVariant: String? = null,
|
||||
val background: String? = null,
|
||||
val surface: String? = null,
|
||||
) {
|
||||
fun toColors(base: DefaultTheme): Colors = when (base) {
|
||||
DefaultTheme.LIGHT -> LightColorPalette.copy(primary = primary?.colorFromReadableHex() ?: LightColorPalette.primary)
|
||||
DefaultTheme.DARK -> DarkColorPalette.copy(primary = primary?.colorFromReadableHex() ?: DarkColorPalette.primary)
|
||||
DefaultTheme.BLUE -> BlueColorPalette.copy(primary = primary?.colorFromReadableHex() ?: BlueColorPalette.primary)
|
||||
// shouldn't be here
|
||||
DefaultTheme.SYSTEM -> LightColorPalette.copy(primary = primary?.colorFromReadableHex() ?: LightColorPalette.primary)
|
||||
fun toColors(base: DefaultTheme): Colors {
|
||||
val baseColors = when (base) {
|
||||
DefaultTheme.LIGHT -> LightColorPalette
|
||||
DefaultTheme.DARK -> DarkColorPalette
|
||||
DefaultTheme.BLUE -> BlueColorPalette
|
||||
// shouldn't be here
|
||||
DefaultTheme.SYSTEM -> LightColorPalette
|
||||
}
|
||||
return baseColors.copy(
|
||||
primary = primary?.colorFromReadableHex() ?: baseColors.primary,
|
||||
primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant,
|
||||
secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary,
|
||||
secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant,
|
||||
background = background?.colorFromReadableHex() ?: baseColors.background,
|
||||
surface = surface?.colorFromReadableHex() ?: baseColors.surface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +98,18 @@ private fun String.colorFromReadableHex(): Color =
|
||||
data class ThemeOverrides (
|
||||
val base: DefaultTheme,
|
||||
val colors: ThemeColors
|
||||
)
|
||||
) {
|
||||
fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides {
|
||||
return copy(colors = when (name) {
|
||||
ThemeColor.PRIMARY -> colors.copy(primary = color)
|
||||
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
|
||||
ThemeColor.SECONDARY -> colors.copy(secondary = color)
|
||||
ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color)
|
||||
ThemeColor.BACKGROUND -> colors.copy(background = color)
|
||||
ThemeColor.SURFACE -> colors.copy(surface = color)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
|
||||
return if (baseTheme == DefaultTheme.BLUE) {
|
||||
|
||||
@@ -26,17 +26,17 @@ object ThemeManager {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val themeOverrides = appPrefs.themeOverrides.get()
|
||||
|
||||
val theme = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
themeOverrides[themeName]
|
||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
themeName
|
||||
} else {
|
||||
themeOverrides[if (darkForSystemTheme) appPrefs.systemDarkTheme.get() else DefaultTheme.LIGHT.name]
|
||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
||||
}
|
||||
val baseTheme = when (themeName) {
|
||||
DefaultTheme.SYSTEM.name -> if (darkForSystemTheme) systemDarkThemeColors() else Pair(LightColorPalette, DefaultTheme.LIGHT)
|
||||
val theme = themeOverrides[nonSystemThemeName]
|
||||
val baseTheme = when (nonSystemThemeName) {
|
||||
DefaultTheme.LIGHT.name -> Pair(LightColorPalette, DefaultTheme.LIGHT)
|
||||
DefaultTheme.DARK.name -> Pair(DarkColorPalette, DefaultTheme.DARK)
|
||||
DefaultTheme.BLUE.name -> Pair(BlueColorPalette, DefaultTheme.BLUE)
|
||||
else -> if (theme != null) theme.colors.toColors(theme.base) to theme.base else Pair(LightColorPalette, DefaultTheme.LIGHT)
|
||||
else -> Pair(LightColorPalette, DefaultTheme.LIGHT)
|
||||
}
|
||||
if (theme == null) {
|
||||
return ActiveTheme(themeName, baseTheme.second, baseTheme.first)
|
||||
@@ -44,6 +44,17 @@ object ThemeManager {
|
||||
return ActiveTheme(themeName, baseTheme.second, theme.colors.toColors(theme.base))
|
||||
}
|
||||
|
||||
fun currentThemeOverrides(darkForSystemTheme: Boolean): ThemeOverrides {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
themeName
|
||||
} else {
|
||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
||||
}
|
||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
||||
return overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
}
|
||||
|
||||
// colors, default theme enum, localized name of theme
|
||||
fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> {
|
||||
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
|
||||
@@ -88,24 +99,50 @@ object ThemeManager {
|
||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
||||
}
|
||||
|
||||
fun saveAndApplyPrimaryColor(color: Color? = null, darkForSystemTheme: Boolean) {
|
||||
fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
themeName
|
||||
} else {
|
||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
||||
}
|
||||
val color = color ?: when(themeName) {
|
||||
DefaultTheme.LIGHT.name -> LightColorPalette.primary
|
||||
DefaultTheme.DARK.name -> DarkColorPalette.primary
|
||||
DefaultTheme.BLUE.name -> BlueColorPalette.primary
|
||||
else -> (if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette).primary
|
||||
var colorToSet = color
|
||||
if (colorToSet == null) {
|
||||
// Setting default color from a base theme
|
||||
colorToSet = when(nonSystemThemeName) {
|
||||
DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette)
|
||||
DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette)
|
||||
DefaultTheme.BLUE.name -> name.fromColors(BlueColorPalette)
|
||||
// Will not be here
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
||||
val prevValue = overrides[nonSystemThemeName]
|
||||
val current = prevValue?.copy(colors = prevValue.colors.copy(primary = color.toReadableHex()))
|
||||
?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors(primary = color.toReadableHex()))
|
||||
overrides[nonSystemThemeName] = current
|
||||
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex())
|
||||
appPrefs.themeOverrides.set(overrides)
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
||||
}
|
||||
|
||||
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) {
|
||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
||||
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
overrides[theme.base.name] = prevValue.copy(colors = theme.colors)
|
||||
appPrefs.themeOverrides.set(overrides)
|
||||
appPrefs.currentTheme.set(theme.base.name)
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
||||
}
|
||||
|
||||
fun resetAllThemeColors(darkForSystemTheme: Boolean) {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
themeName
|
||||
} else {
|
||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
||||
}
|
||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
||||
val prevValue = overrides[nonSystemThemeName] ?: return
|
||||
overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors())
|
||||
appPrefs.themeOverrides.set(overrides)
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package chat.simplex.app.views.helpers
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -42,14 +44,19 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U
|
||||
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, withPadding: Boolean = true) {
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val brush = if (theme.value.base == DefaultTheme.BLUE)
|
||||
Brush.linearGradient(listOf(Color(0xff1068D9), Color(0xff41A9F5)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||
else // color is not updated when changing themes if I pass null here
|
||||
Brush.linearGradient(listOf(MaterialTheme.colors.primaryVariant, MaterialTheme.colors.primaryVariant), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||
Text(
|
||||
title,
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = DEFAULT_PADDING * 1.5f, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1,
|
||||
color = if (CurrentColors.collectAsState().value.base == DefaultTheme.BLUE) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.primary,
|
||||
style = MaterialTheme.typography.h1.copy(brush = brush),
|
||||
color = MaterialTheme.colors.primaryVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import androidx.core.text.HtmlCompat
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.ThemeOverrides
|
||||
import com.charleskorn.kaml.decodeFromStream
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -388,6 +390,22 @@ fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable
|
||||
}
|
||||
}
|
||||
|
||||
fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeOverrides? {
|
||||
SimplexApp.context.contentResolver.openInputStream(uri).use {
|
||||
runCatching {
|
||||
return yaml.decodeFromStream<ThemeOverrides>(it!!)
|
||||
}.onFailure {
|
||||
if (withAlertOnException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.import_theme_error),
|
||||
text = generalGetString(R.string.import_theme_error_desc),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveImage(context: Context, uri: Uri): String? {
|
||||
val bitmap = getBitmapFromUri(uri) ?: return null
|
||||
return saveImage(context, bitmap)
|
||||
|
||||
@@ -3,14 +3,22 @@ package chat.simplex.app.views.usersettings
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -32,12 +40,13 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.SharedPreference
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.godaddy.android.colorpicker.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.BufferedOutputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -72,9 +81,9 @@ fun AppearanceView(m: ChatModel) {
|
||||
m.controller.appPrefs.appLanguage,
|
||||
m.controller.appPrefs.systemDarkTheme,
|
||||
changeIcon = ::setAppIcon,
|
||||
editPrimaryColor = { primary ->
|
||||
editColor = { name, initialColor ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ColorEditor(primary, close)
|
||||
ColorEditor(name, initialColor, close)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -85,7 +94,7 @@ fun AppearanceView(m: ChatModel) {
|
||||
languagePref: SharedPreference<String?>,
|
||||
systemDarkTheme: SharedPreference<String?>,
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
editPrimaryColor: (Color) -> Unit,
|
||||
editColor: (ThemeColor, Color) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
@@ -152,31 +161,78 @@ fun AppearanceView(m: ChatModel) {
|
||||
ThemeManager.applyTheme(it, darkTheme)
|
||||
}
|
||||
if (state.value == DefaultTheme.SYSTEM.name) {
|
||||
val systemDarkTheme = remember { systemDarkTheme.state }
|
||||
PreferenceToggle(generalGetString(R.string.simplex_blue_as_dark_theme), systemDarkTheme.value == DefaultTheme.BLUE.name) {
|
||||
ThemeManager.changeDarkTheme(if (it) DefaultTheme.BLUE.name else DefaultTheme.DARK.name, darkTheme)
|
||||
}
|
||||
/*DarkThemeSelector(systemDarkTheme) {
|
||||
DarkThemeSelector(remember { systemDarkTheme.state }) {
|
||||
ThemeManager.changeDarkTheme(it, darkTheme)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.colors.primary) }) {
|
||||
SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) {
|
||||
val title = generalGetString(R.string.color_primary)
|
||||
Text(title)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.primary)
|
||||
}
|
||||
}
|
||||
if (currentTheme.base.hasChangedPrimary(currentTheme.colors)) {
|
||||
SectionCustomFooter(PaddingValues(start = 7.dp, end = 7.dp, top = 5.dp)) {
|
||||
val isInDarkTheme = isInDarkTheme()
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyPrimaryColor(darkForSystemTheme = isInDarkTheme)
|
||||
},
|
||||
) {
|
||||
Text(generalGetString(R.string.reset_color))
|
||||
// Not allowed to change title color since it is using gradient brush with custom colors
|
||||
if (currentTheme.base != DefaultTheme.BLUE) {
|
||||
SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) {
|
||||
val title = generalGetString(R.string.color_primary_variant)
|
||||
Text(title)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.primaryVariant)
|
||||
}
|
||||
}
|
||||
SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) {
|
||||
val title = generalGetString(R.string.color_secondary)
|
||||
Text(title)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.secondary)
|
||||
}
|
||||
// Not using it yet
|
||||
/*SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) {
|
||||
val title = generalGetString(R.string.color_secondary_variant)
|
||||
Text(title)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.secondaryVariant)
|
||||
}*/
|
||||
SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) {
|
||||
val title = generalGetString(R.string.color_background)
|
||||
Text(title)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.background)
|
||||
}
|
||||
SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) {
|
||||
val title = generalGetString(R.string.color_surface)
|
||||
Text(title)
|
||||
Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.surface)
|
||||
}
|
||||
}
|
||||
val isInDarkTheme = isInDarkTheme()
|
||||
if (currentTheme.base.hasChangedAnyColor(currentTheme.colors)) {
|
||||
SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) {
|
||||
Text(generalGetString(R.string.reset_color), color = colors.error)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
if (currentTheme.base.hasChangedAnyColor(currentTheme.colors)) {
|
||||
val context = LocalContext.current
|
||||
val theme = remember { mutableStateOf(null as String?) }
|
||||
val exportThemeLauncher = rememberSaveThemeLauncher(context, theme)
|
||||
SectionItemView({
|
||||
val overrides = ThemeManager.currentThemeOverrides(isInDarkTheme)
|
||||
theme.value = yaml.encodeToString<ThemeOverrides>(overrides)
|
||||
exportThemeLauncher.launch("SimpleX-${overrides.base.name}.yml")
|
||||
}) {
|
||||
Text(generalGetString(R.string.export_theme), color = colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
val importThemeLauncher = rememberGetContentLauncher { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val theme = getThemeFromUri(uri)
|
||||
if (theme != null) {
|
||||
ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Can not limit to YAML mime type since it's unsupported by Android
|
||||
SectionItemView({ importThemeLauncher.launch("*/*") }) {
|
||||
Text(generalGetString(R.string.import_theme), color = colors.error)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
@@ -184,6 +240,7 @@ fun AppearanceView(m: ChatModel) {
|
||||
|
||||
@Composable
|
||||
fun ColorEditor(
|
||||
name: ThemeColor,
|
||||
initialColor: Color,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
@@ -191,7 +248,7 @@ fun ColorEditor(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.color_primary))
|
||||
AppBarTitle(name.text)
|
||||
var currentColor by remember { mutableStateOf(initialColor) }
|
||||
ColorPicker(initialColor) {
|
||||
currentColor = it
|
||||
@@ -201,7 +258,7 @@ fun ColorEditor(
|
||||
val isInDarkTheme = isInDarkTheme()
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyPrimaryColor(currentColor, isInDarkTheme)
|
||||
ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme)
|
||||
close()
|
||||
},
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
@@ -289,6 +346,33 @@ private fun DarkThemeSelector(state: State<String?>, onSelected: (String) -> Uni
|
||||
// activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName)))
|
||||
//}
|
||||
|
||||
@Composable
|
||||
private fun rememberSaveThemeLauncher(cxt: Context, theme: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(),
|
||||
onResult = { destination ->
|
||||
try {
|
||||
destination?.let {
|
||||
val theme = theme.value
|
||||
if (theme != null) {
|
||||
val contentResolver = cxt.contentResolver
|
||||
contentResolver.openOutputStream(destination)?.let { stream ->
|
||||
BufferedOutputStream(stream).use { outputStream ->
|
||||
theme.byteInputStream().use { it.copyTo(outputStream) }
|
||||
}
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Error) {
|
||||
Toast.makeText(cxt, generalGetString(R.string.error_saving_file), Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, "rememberSaveThemeLauncher error saving theme $e")
|
||||
} finally {
|
||||
theme.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
|
||||
SimplexApp.context.packageManager.getComponentEnabledSetting(
|
||||
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
|
||||
@@ -304,7 +388,7 @@ fun PreviewAppearanceSettings() {
|
||||
languagePref = SharedPreference({ null }, {}),
|
||||
systemDarkTheme = SharedPreference({ null }, {}),
|
||||
changeIcon = {},
|
||||
editPrimaryColor = {},
|
||||
editColor = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1163,11 +1163,19 @@
|
||||
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Theme</string>
|
||||
<string name="simplex_blue_as_dark_theme">Blue in dark mode</string>
|
||||
<string name="dark_theme" translatable="false">Dark theme</string>
|
||||
<string name="dark_theme">Dark theme</string>
|
||||
<string name="save_color">Save color</string>
|
||||
<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="export_theme">Export theme</string>
|
||||
<string name="reset_color">Reset colors</string>
|
||||
<string name="color_primary">Accent</string>
|
||||
<string name="color_primary_variant">Additional accent</string>
|
||||
<string name="color_secondary">Secondary</string>
|
||||
<string name="color_secondary_variant">Additional secondary</string>
|
||||
<string name="color_background">Background</string>
|
||||
<string name="color_surface">Surface</string>
|
||||
|
||||
<!-- Preferences.kt -->
|
||||
<string name="chat_preferences_you_allow">You allow</string>
|
||||
|
||||
Reference in New Issue
Block a user