From 1efc7aafd86faeca2f6fe4be43cf70f9e15f147c Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Thu, 27 Apr 2023 17:33:15 +0700 Subject: [PATCH] more color options and better code --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 7 + .../java/chat/simplex/app/ui/theme/Theme.kt | 89 ++++++++++-- .../chat/simplex/app/ui/theme/ThemeManager.kt | 69 ++++++--- .../app/views/helpers/CloseSheetBar.kt | 15 +- .../chat/simplex/app/views/helpers/Util.kt | 18 +++ .../app/views/usersettings/Appearance.kt | 134 ++++++++++++++---- .../app/src/main/res/values/strings.xml | 12 +- 8 files changed, 284 insertions(+), 61 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 0a256bbb99..2291fcd1dd 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -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" diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index a9196e62e5..71d95b7558 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -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 { diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index 80453b11b2..1165eb052c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -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) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt index 6a7174b477..ec3331a63a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt @@ -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> { val allThemes = ArrayList>() @@ -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) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt index d513e48a1e..44ec4421fb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -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 ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 0a274d20b9..711e2aa3b2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -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(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) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt index 9d0c454d1c..2aeddccd67 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt @@ -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, systemDarkTheme: SharedPreference, 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(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, 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): ManagedActivityResultLauncher = + 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 = { _, _ -> }, ) } } diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 3adab34212..3ad9947753 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -1163,11 +1163,19 @@ Theme - Blue in dark mode - Dark theme + Dark theme Save color + Import theme + Import theme error + Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure. + Export theme Reset colors Accent + Additional accent + Secondary + Additional secondary + Background + Surface You allow