mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Merge pull request #3412 from vitorpamplona/claude/app-ui-theme-customization-evwwkb
Add customizable accent colors, fonts, and font sizes
This commit is contained in:
@@ -54,6 +54,9 @@ data class UiSettings(
|
||||
val showProfileFollowersFeed: Boolean = true,
|
||||
val dontShowOnchainPublicWarning: Boolean = false,
|
||||
val suggestWorkoutsFromHealthConnect: BooleanType = BooleanType.ALWAYS,
|
||||
val accentColor: AccentColorType = AccentColorType.PURPLE,
|
||||
val fontFamily: FontFamilyType = FontFamilyType.SYSTEM,
|
||||
val fontSize: FontSizeType = FontSizeType.NORMAL,
|
||||
)
|
||||
|
||||
enum class ThemeType(
|
||||
@@ -73,6 +76,68 @@ fun parseThemeType(code: Int?): ThemeType =
|
||||
else -> ThemeType.SYSTEM
|
||||
}
|
||||
|
||||
enum class AccentColorType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
PURPLE(0, R.string.accent_color_purple),
|
||||
BLUE(1, R.string.accent_color_blue),
|
||||
GREEN(2, R.string.accent_color_green),
|
||||
ORANGE(3, R.string.accent_color_orange),
|
||||
RED(4, R.string.accent_color_red),
|
||||
PINK(5, R.string.accent_color_pink),
|
||||
}
|
||||
|
||||
fun parseAccentColorType(screenCode: Int): AccentColorType =
|
||||
when (screenCode) {
|
||||
AccentColorType.PURPLE.screenCode -> AccentColorType.PURPLE
|
||||
AccentColorType.BLUE.screenCode -> AccentColorType.BLUE
|
||||
AccentColorType.GREEN.screenCode -> AccentColorType.GREEN
|
||||
AccentColorType.ORANGE.screenCode -> AccentColorType.ORANGE
|
||||
AccentColorType.RED.screenCode -> AccentColorType.RED
|
||||
AccentColorType.PINK.screenCode -> AccentColorType.PINK
|
||||
else -> AccentColorType.PURPLE
|
||||
}
|
||||
|
||||
enum class FontFamilyType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
SYSTEM(0, R.string.font_family_system),
|
||||
SANS_SERIF(1, R.string.font_family_sans_serif),
|
||||
SERIF(2, R.string.font_family_serif),
|
||||
MONOSPACE(3, R.string.font_family_monospace),
|
||||
}
|
||||
|
||||
fun parseFontFamilyType(screenCode: Int): FontFamilyType =
|
||||
when (screenCode) {
|
||||
FontFamilyType.SYSTEM.screenCode -> FontFamilyType.SYSTEM
|
||||
FontFamilyType.SANS_SERIF.screenCode -> FontFamilyType.SANS_SERIF
|
||||
FontFamilyType.SERIF.screenCode -> FontFamilyType.SERIF
|
||||
FontFamilyType.MONOSPACE.screenCode -> FontFamilyType.MONOSPACE
|
||||
else -> FontFamilyType.SYSTEM
|
||||
}
|
||||
|
||||
enum class FontSizeType(
|
||||
val scale: Float,
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
SMALL(0.85f, 0, R.string.font_size_small),
|
||||
NORMAL(1.0f, 1, R.string.font_size_normal),
|
||||
LARGE(1.15f, 2, R.string.font_size_large),
|
||||
HUGE(1.3f, 3, R.string.font_size_huge),
|
||||
}
|
||||
|
||||
fun parseFontSizeType(screenCode: Int): FontSizeType =
|
||||
when (screenCode) {
|
||||
FontSizeType.SMALL.screenCode -> FontSizeType.SMALL
|
||||
FontSizeType.NORMAL.screenCode -> FontSizeType.NORMAL
|
||||
FontSizeType.LARGE.screenCode -> FontSizeType.LARGE
|
||||
FontSizeType.HUGE.screenCode -> FontSizeType.HUGE
|
||||
else -> FontSizeType.NORMAL
|
||||
}
|
||||
|
||||
enum class ConnectivityType(
|
||||
val prefCode: Boolean?,
|
||||
val screenCode: Int,
|
||||
|
||||
@@ -54,6 +54,9 @@ class UiSettingsFlow(
|
||||
val showProfileFollowersFeed: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val dontShowOnchainPublicWarning: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val suggestWorkoutsFromHealthConnect: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val accentColor: MutableStateFlow<AccentColorType> = MutableStateFlow(AccentColorType.PURPLE),
|
||||
val fontFamily: MutableStateFlow<FontFamilyType> = MutableStateFlow(FontFamilyType.SYSTEM),
|
||||
val fontSize: MutableStateFlow<FontSizeType> = MutableStateFlow(FontSizeType.NORMAL),
|
||||
) {
|
||||
val listOfFlows: List<Flow<Any?>> =
|
||||
listOf<Flow<Any?>>(
|
||||
@@ -82,6 +85,9 @@ class UiSettingsFlow(
|
||||
showProfileFollowersFeed,
|
||||
dontShowOnchainPublicWarning,
|
||||
suggestWorkoutsFromHealthConnect,
|
||||
accentColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
)
|
||||
|
||||
// emits at every change in any of the propertyes.
|
||||
@@ -114,6 +120,9 @@ class UiSettingsFlow(
|
||||
flows[22] as Boolean,
|
||||
flows[23] as Boolean,
|
||||
flows[24] as BooleanType,
|
||||
flows[25] as AccentColorType,
|
||||
flows[26] as FontFamilyType,
|
||||
flows[27] as FontSizeType,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,6 +153,9 @@ class UiSettingsFlow(
|
||||
showProfileFollowersFeed.value,
|
||||
dontShowOnchainPublicWarning.value,
|
||||
suggestWorkoutsFromHealthConnect.value,
|
||||
accentColor.value,
|
||||
fontFamily.value,
|
||||
fontSize.value,
|
||||
)
|
||||
|
||||
fun update(torSettings: UiSettings): Boolean {
|
||||
@@ -249,6 +261,18 @@ class UiSettingsFlow(
|
||||
suggestWorkoutsFromHealthConnect.tryEmit(torSettings.suggestWorkoutsFromHealthConnect)
|
||||
any = true
|
||||
}
|
||||
if (accentColor.value != torSettings.accentColor) {
|
||||
accentColor.tryEmit(torSettings.accentColor)
|
||||
any = true
|
||||
}
|
||||
if (fontFamily.value != torSettings.fontFamily) {
|
||||
fontFamily.tryEmit(torSettings.fontFamily)
|
||||
any = true
|
||||
}
|
||||
if (fontSize.value != torSettings.fontSize) {
|
||||
fontSize.tryEmit(torSettings.fontSize)
|
||||
any = true
|
||||
}
|
||||
|
||||
return any
|
||||
}
|
||||
@@ -299,6 +323,9 @@ class UiSettingsFlow(
|
||||
MutableStateFlow(uiSettings.showProfileFollowersFeed),
|
||||
MutableStateFlow(uiSettings.dontShowOnchainPublicWarning),
|
||||
MutableStateFlow(uiSettings.suggestWorkoutsFromHealthConnect),
|
||||
MutableStateFlow(uiSettings.accentColor),
|
||||
MutableStateFlow(uiSettings.fontFamily),
|
||||
MutableStateFlow(uiSettings.fontSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -31,9 +31,12 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.AccentColorType
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.FontFamilyType
|
||||
import com.vitorpamplona.amethyst.model.FontSizeType
|
||||
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
@@ -120,6 +123,9 @@ class UiSharedPreferences(
|
||||
val UI_SHOW_PROFILE_FOLLOWERS_FEED = booleanPreferencesKey("ui.show_profile_followers_feed")
|
||||
val UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING = booleanPreferencesKey("ui.dont_show_onchain_public_warning")
|
||||
val UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT = stringPreferencesKey("ui.suggest_workouts_from_health_connect")
|
||||
val UI_ACCENT_COLOR = stringPreferencesKey("ui.accent_color")
|
||||
val UI_FONT_FAMILY = stringPreferencesKey("ui.font_family")
|
||||
val UI_FONT_SIZE = stringPreferencesKey("ui.font_size")
|
||||
|
||||
suspend fun uiPreferences(context: Context): UiSettings? =
|
||||
try {
|
||||
@@ -157,6 +163,9 @@ class UiSharedPreferences(
|
||||
dontShowOnchainPublicWarning = preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] ?: false,
|
||||
suggestWorkoutsFromHealthConnect =
|
||||
preferences[UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
|
||||
accentColor = preferences[UI_ACCENT_COLOR]?.let { AccentColorType.valueOf(it) } ?: AccentColorType.PURPLE,
|
||||
fontFamily = preferences[UI_FONT_FAMILY]?.let { FontFamilyType.valueOf(it) } ?: FontFamilyType.SYSTEM,
|
||||
fontSize = preferences[UI_FONT_SIZE]?.let { FontSizeType.valueOf(it) } ?: FontSizeType.NORMAL,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -206,6 +215,9 @@ class UiSharedPreferences(
|
||||
preferences[UI_SHOW_PROFILE_FOLLOWERS_FEED] = sharedSettings.showProfileFollowersFeed
|
||||
preferences[UI_DONT_SHOW_ONCHAIN_PUBLIC_WARNING] = sharedSettings.dontShowOnchainPublicWarning
|
||||
preferences[UI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT] = sharedSettings.suggestWorkoutsFromHealthConnect.name
|
||||
preferences[UI_ACCENT_COLOR] = sharedSettings.accentColor.name
|
||||
preferences[UI_FONT_FAMILY] = sharedSettings.fontFamily.name
|
||||
preferences[UI_FONT_SIZE] = sharedSettings.fontSize.name
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
+77
-23
@@ -48,15 +48,19 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AccentColorType
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
||||
import com.vitorpamplona.amethyst.model.FontFamilyType
|
||||
import com.vitorpamplona.amethyst.model.FontSizeType
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.model.parseAccentColorType
|
||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.parseFeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.parseGalleryType
|
||||
import com.vitorpamplona.amethyst.model.parseFontFamilyType
|
||||
import com.vitorpamplona.amethyst.model.parseFontSizeType
|
||||
import com.vitorpamplona.amethyst.model.parseThemeType
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
@@ -113,6 +117,9 @@ fun SettingsScreen(sharedPrefs: UiSettingsFlow) {
|
||||
) {
|
||||
ShowLanguageChoice(sharedPrefs)
|
||||
ShowThemeChoice(sharedPrefs)
|
||||
ShowAccentColorChoice(sharedPrefs)
|
||||
ShowFontFamilyChoice(sharedPrefs)
|
||||
ShowFontSizeChoice(sharedPrefs)
|
||||
ShowImagePreviewChoice(sharedPrefs)
|
||||
ShowVideoPlaybackChoice(sharedPrefs)
|
||||
AutoplayVideosChoice(sharedPrefs)
|
||||
@@ -120,7 +127,6 @@ fun SettingsScreen(sharedPrefs: UiSettingsFlow) {
|
||||
ShowProfilePictureChoice(sharedPrefs)
|
||||
ImmersiveScrollingChoice(sharedPrefs)
|
||||
FeatureSetChoice(sharedPrefs)
|
||||
GalleryChoice(sharedPrefs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +224,74 @@ fun ShowThemeChoice(sharedPrefs: UiSettingsFlow) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowAccentColorChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val accentOptions =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(AccentColorType.PURPLE.resourceId)),
|
||||
TitleExplainer(stringRes(AccentColorType.BLUE.resourceId)),
|
||||
TitleExplainer(stringRes(AccentColorType.GREEN.resourceId)),
|
||||
TitleExplainer(stringRes(AccentColorType.ORANGE.resourceId)),
|
||||
TitleExplainer(stringRes(AccentColorType.RED.resourceId)),
|
||||
TitleExplainer(stringRes(AccentColorType.PINK.resourceId)),
|
||||
)
|
||||
|
||||
val accentIndex by sharedPrefs.accentColor.collectAsState()
|
||||
|
||||
SettingsRow(
|
||||
R.string.accent_color,
|
||||
R.string.accent_color_description,
|
||||
accentOptions,
|
||||
accentIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.accentColor.tryEmit(parseAccentColorType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowFontFamilyChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val fontOptions =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(FontFamilyType.SYSTEM.resourceId)),
|
||||
TitleExplainer(stringRes(FontFamilyType.SANS_SERIF.resourceId)),
|
||||
TitleExplainer(stringRes(FontFamilyType.SERIF.resourceId)),
|
||||
TitleExplainer(stringRes(FontFamilyType.MONOSPACE.resourceId)),
|
||||
)
|
||||
|
||||
val fontIndex by sharedPrefs.fontFamily.collectAsState()
|
||||
|
||||
SettingsRow(
|
||||
R.string.font_family,
|
||||
R.string.font_family_description,
|
||||
fontOptions,
|
||||
fontIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.fontFamily.tryEmit(parseFontFamilyType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowFontSizeChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val fontSizeOptions =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(FontSizeType.SMALL.resourceId)),
|
||||
TitleExplainer(stringRes(FontSizeType.NORMAL.resourceId)),
|
||||
TitleExplainer(stringRes(FontSizeType.LARGE.resourceId)),
|
||||
TitleExplainer(stringRes(FontSizeType.HUGE.resourceId)),
|
||||
)
|
||||
|
||||
val fontSizeIndex by sharedPrefs.fontSize.collectAsState()
|
||||
|
||||
SettingsRow(
|
||||
R.string.font_size,
|
||||
R.string.font_size_description,
|
||||
fontSizeOptions,
|
||||
fontSizeIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.fontSize.tryEmit(parseFontSizeType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowImagePreviewChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val connectivityBasedOptions =
|
||||
@@ -363,26 +437,6 @@ fun FeatureSetChoice(sharedPrefs: UiSettingsFlow) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val galleryItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)),
|
||||
TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)),
|
||||
)
|
||||
|
||||
val galleryIndex by sharedPrefs.gallerySet.collectAsState()
|
||||
|
||||
SettingsRow(
|
||||
R.string.gallery_style,
|
||||
R.string.gallery_style_description,
|
||||
galleryItems,
|
||||
galleryIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.gallerySet.tryEmit(parseGalleryType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsRow(
|
||||
name: Int,
|
||||
|
||||
+30
@@ -43,6 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.model.parseGalleryType
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
@@ -51,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -129,11 +134,36 @@ fun ProfileUiSettingsContent(accountViewModel: AccountViewModel) {
|
||||
checked = showFollowers,
|
||||
onCheckedChange = { ui.showProfileFollowersFeed.tryEmit(it) },
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
Column(modifier = Modifier.padding(vertical = 12.dp, horizontal = Size20dp)) {
|
||||
GalleryChoice(ui)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val galleryItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)),
|
||||
TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)),
|
||||
)
|
||||
|
||||
val galleryIndex by sharedPrefs.gallerySet.collectAsStateWithLifecycle()
|
||||
|
||||
SettingsRow(
|
||||
R.string.gallery_style,
|
||||
R.string.gallery_style_description,
|
||||
galleryItems,
|
||||
galleryIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.gallerySet.tryEmit(parseGalleryType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileUiSwitchRow(
|
||||
title: String,
|
||||
|
||||
@@ -36,6 +36,19 @@ val Purple200 = Color(0xFFBB86FC)
|
||||
val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
|
||||
// Accent palette options selected through Settings -> Accent Color.
|
||||
// Each accent ships a brighter variant for the dark theme and a deeper variant for the light theme.
|
||||
val AccentBlueDark = Color(0xFF82B1FF)
|
||||
val AccentBlueLight = Color(0xFF1565C0)
|
||||
val AccentGreenDark = Color(0xFF80CBC4)
|
||||
val AccentGreenLight = Color(0xFF2E7D32)
|
||||
val AccentOrangeDark = Color(0xFFFFB74D)
|
||||
val AccentOrangeLight = Color(0xFFE65100)
|
||||
val AccentRedDark = Color(0xFFEF9A9A)
|
||||
val AccentRedLight = Color(0xFFC62828)
|
||||
val AccentPinkDark = Color(0xFFF48FB1)
|
||||
val AccentPinkLight = Color(0xFFAD1457)
|
||||
val BitcoinOrange = Color(0xFFF7931A)
|
||||
val RoyalBlue = Color(0xFF4169E1)
|
||||
|
||||
|
||||
@@ -31,12 +31,15 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -45,11 +48,13 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -62,48 +67,58 @@ import com.patrykandpatrick.vico.compose.common.VicoTheme
|
||||
import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideMaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.AccentColorType
|
||||
import com.vitorpamplona.amethyst.model.FontFamilyType
|
||||
import com.vitorpamplona.amethyst.model.FontSizeType
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
|
||||
private val DarkColorPalette =
|
||||
// The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color.
|
||||
// Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent
|
||||
// uses its single hue across primary and secondary for a cohesive single-color theme.
|
||||
private fun accentPrimary(
|
||||
accent: AccentColorType,
|
||||
dark: Boolean,
|
||||
): Color =
|
||||
when (accent) {
|
||||
AccentColorType.PURPLE -> if (dark) Purple200 else Purple500
|
||||
AccentColorType.BLUE -> if (dark) AccentBlueDark else AccentBlueLight
|
||||
AccentColorType.GREEN -> if (dark) AccentGreenDark else AccentGreenLight
|
||||
AccentColorType.ORANGE -> if (dark) AccentOrangeDark else AccentOrangeLight
|
||||
AccentColorType.RED -> if (dark) AccentRedDark else AccentRedLight
|
||||
AccentColorType.PINK -> if (dark) AccentPinkDark else AccentPinkLight
|
||||
}
|
||||
|
||||
private fun accentSecondary(
|
||||
accent: AccentColorType,
|
||||
dark: Boolean,
|
||||
): Color = if (accent == AccentColorType.PURPLE) Teal200 else accentPrimary(accent, dark)
|
||||
|
||||
private fun darkColors(accent: AccentColorType) =
|
||||
darkColorScheme(
|
||||
primary = Purple200,
|
||||
secondary = Teal200,
|
||||
tertiary = Teal200,
|
||||
primary = accentPrimary(accent, dark = true),
|
||||
secondary = accentSecondary(accent, dark = true),
|
||||
tertiary = accentSecondary(accent, dark = true),
|
||||
background = Color.Black,
|
||||
surface = Color.Black,
|
||||
surfaceDim = Color.Black,
|
||||
surfaceVariant = Color(red = 29, green = 26, blue = 34),
|
||||
)
|
||||
|
||||
private val LightColorPalette =
|
||||
private fun lightColors(accent: AccentColorType) =
|
||||
lightColorScheme(
|
||||
primary = Purple500,
|
||||
secondary = Teal200,
|
||||
tertiary = Teal200,
|
||||
primary = accentPrimary(accent, dark = false),
|
||||
secondary = accentSecondary(accent, dark = false),
|
||||
tertiary = accentSecondary(accent, dark = false),
|
||||
surfaceContainerHighest = Color(red = 236, green = 230, blue = 240),
|
||||
surfaceVariant = Color(red = 250, green = 245, blue = 252),
|
||||
)
|
||||
|
||||
private val DarkNewItemBackground = DarkColorPalette.primary.copy(0.12f)
|
||||
private val LightNewItemBackground = LightColorPalette.primary.copy(0.12f)
|
||||
private val DarkColorPalette = darkColors(AccentColorType.PURPLE)
|
||||
private val LightColorPalette = lightColors(AccentColorType.PURPLE)
|
||||
|
||||
private val DarkTransparentBackground = DarkColorPalette.background.copy(0.32f)
|
||||
private val LightTransparentBackground = LightColorPalette.background.copy(0.32f)
|
||||
|
||||
private val DarkSelectedNote = DarkNewItemBackground.compositeOver(DarkColorPalette.background)
|
||||
private val LightSelectedNote = LightNewItemBackground.compositeOver(LightColorPalette.background)
|
||||
|
||||
private val DarkButtonBackground =
|
||||
DarkColorPalette.primary.copy(alpha = 0.32f).compositeOver(DarkColorPalette.background)
|
||||
private val LightButtonBackground =
|
||||
LightColorPalette.primary.copy(alpha = 0.32f).compositeOver(LightColorPalette.background)
|
||||
|
||||
private val DarkLessImportantLink = DarkColorPalette.primary.copy(alpha = 0.52f)
|
||||
private val LightLessImportantLink = LightColorPalette.primary.copy(alpha = 0.52f)
|
||||
|
||||
private val DarkMediumImportantLink = DarkColorPalette.primary.copy(alpha = 0.32f)
|
||||
private val LightMediumImportantLink = LightColorPalette.primary.copy(alpha = 0.32f)
|
||||
|
||||
private val DarkGrayText = DarkColorPalette.onSurface.copy(alpha = 0.52f)
|
||||
private val LightGrayText = LightColorPalette.onSurface.copy(alpha = 0.52f)
|
||||
|
||||
@@ -407,26 +422,32 @@ val MarkDownStyleOnLight =
|
||||
),
|
||||
)
|
||||
|
||||
// Compared against the dark palette's background instead of a fixed primary so the check keeps
|
||||
// working when the user picks a non-purple accent (accent only changes primary/secondary, never
|
||||
// background). Kept as a single reference comparison because this getter fans out to hundreds of
|
||||
// themed-color call sites on hot rendering paths — luminance()/etc. would add real per-frame cost.
|
||||
val ColorScheme.isLight: Boolean
|
||||
get() = primary == Purple500
|
||||
get() = background != Color.Black
|
||||
|
||||
// The accent-derived tints below are computed from the live scheme's primary so they follow
|
||||
// the selected accent color. Color is an inline value class, so these copies don't allocate.
|
||||
val ColorScheme.newItemBackgroundColor: Color
|
||||
get() = if (isLight) LightNewItemBackground else DarkNewItemBackground
|
||||
get() = primary.copy(alpha = 0.12f)
|
||||
|
||||
val ColorScheme.transparentBackground: Color
|
||||
get() = if (isLight) LightTransparentBackground else DarkTransparentBackground
|
||||
|
||||
val ColorScheme.selectedNote: Color
|
||||
get() = if (isLight) LightSelectedNote else DarkSelectedNote
|
||||
get() = primary.copy(alpha = 0.12f).compositeOver(background)
|
||||
|
||||
val ColorScheme.secondaryButtonBackground: Color
|
||||
get() = if (isLight) LightButtonBackground else DarkButtonBackground
|
||||
get() = primary.copy(alpha = 0.32f).compositeOver(background)
|
||||
|
||||
val ColorScheme.lessImportantLink: Color
|
||||
get() = if (isLight) LightLessImportantLink else DarkLessImportantLink
|
||||
get() = primary.copy(alpha = 0.52f)
|
||||
|
||||
val ColorScheme.mediumImportanceLink: Color
|
||||
get() = if (isLight) LightMediumImportantLink else DarkMediumImportantLink
|
||||
get() = primary.copy(alpha = 0.32f)
|
||||
|
||||
val ColorScheme.placeholderText: Color
|
||||
get() = if (isLight) LightPlaceholderText else DarkPlaceholderText
|
||||
@@ -562,15 +583,21 @@ val ColorScheme.chartStyle: VicoTheme
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(content: @Composable () -> Unit) {
|
||||
val theme by Amethyst.instance.uiPrefs.value.theme
|
||||
.collectAsStateWithLifecycle()
|
||||
val uiPrefs = Amethyst.instance.uiPrefs.value
|
||||
val theme by uiPrefs.theme.collectAsStateWithLifecycle()
|
||||
val accentColor by uiPrefs.accentColor.collectAsStateWithLifecycle()
|
||||
val fontFamily by uiPrefs.fontFamily.collectAsStateWithLifecycle()
|
||||
val fontSize by uiPrefs.fontSize.collectAsStateWithLifecycle()
|
||||
|
||||
AmethystTheme(theme, content)
|
||||
AmethystTheme(theme, accentColor, fontFamily, fontSize, content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(
|
||||
prefTheme: ThemeType,
|
||||
accentColor: AccentColorType = AccentColorType.PURPLE,
|
||||
fontFamily: FontFamilyType = FontFamilyType.SYSTEM,
|
||||
fontSize: FontSizeType = FontSizeType.NORMAL,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -592,13 +619,33 @@ fun AmethystTheme(
|
||||
isSystemInDarkTheme()
|
||||
}
|
||||
}
|
||||
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
|
||||
val colors =
|
||||
remember(darkTheme, accentColor) {
|
||||
if (darkTheme) darkColors(accentColor) else lightColors(accentColor)
|
||||
}
|
||||
|
||||
val resolvedFontFamily = remember(fontFamily) { fontFamily.toFontFamily() }
|
||||
val typography = remember(fontFamily) { Typography.withFontFamily(resolvedFontFamily) }
|
||||
|
||||
val density = LocalDensity.current
|
||||
val scaledDensity =
|
||||
remember(density, fontSize) {
|
||||
Density(density.density, density.fontScale * fontSize.scale)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
typography = Typography,
|
||||
typography = typography,
|
||||
shapes = Shapes,
|
||||
content = { ProvideMaterialSymbols(content = content) },
|
||||
content = {
|
||||
ProvideMaterialSymbols {
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides scaledDensity,
|
||||
LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val view = LocalView.current
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.halilibo.richtext.ui.HeadingStyle
|
||||
import com.vitorpamplona.amethyst.model.FontFamilyType
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography =
|
||||
@@ -52,6 +53,39 @@ val Typography =
|
||||
*/
|
||||
)
|
||||
|
||||
// Maps the user-selected font preference to a Compose [FontFamily].
|
||||
// SYSTEM returns null so the platform default is used unchanged.
|
||||
fun FontFamilyType.toFontFamily(): FontFamily? =
|
||||
when (this) {
|
||||
FontFamilyType.SYSTEM -> null
|
||||
FontFamilyType.SANS_SERIF -> FontFamily.SansSerif
|
||||
FontFamilyType.SERIF -> FontFamily.Serif
|
||||
FontFamilyType.MONOSPACE -> FontFamily.Monospace
|
||||
}
|
||||
|
||||
// Applies the chosen [FontFamily] to every text style so Material components pick it up too.
|
||||
// A null family leaves the typography untouched (platform default).
|
||||
fun Typography.withFontFamily(fontFamily: FontFamily?): Typography {
|
||||
if (fontFamily == null) return this
|
||||
return copy(
|
||||
displayLarge = displayLarge.copy(fontFamily = fontFamily),
|
||||
displayMedium = displayMedium.copy(fontFamily = fontFamily),
|
||||
displaySmall = displaySmall.copy(fontFamily = fontFamily),
|
||||
headlineLarge = headlineLarge.copy(fontFamily = fontFamily),
|
||||
headlineMedium = headlineMedium.copy(fontFamily = fontFamily),
|
||||
headlineSmall = headlineSmall.copy(fontFamily = fontFamily),
|
||||
titleLarge = titleLarge.copy(fontFamily = fontFamily),
|
||||
titleMedium = titleMedium.copy(fontFamily = fontFamily),
|
||||
titleSmall = titleSmall.copy(fontFamily = fontFamily),
|
||||
bodyLarge = bodyLarge.copy(fontFamily = fontFamily),
|
||||
bodyMedium = bodyMedium.copy(fontFamily = fontFamily),
|
||||
bodySmall = bodySmall.copy(fontFamily = fontFamily),
|
||||
labelLarge = labelLarge.copy(fontFamily = fontFamily),
|
||||
labelMedium = labelMedium.copy(fontFamily = fontFamily),
|
||||
labelSmall = labelSmall.copy(fontFamily = fontFamily),
|
||||
)
|
||||
}
|
||||
|
||||
val Font4SP = 4.sp
|
||||
val Font6SP = 6.sp
|
||||
val Font8SP = 8.sp
|
||||
|
||||
@@ -1624,6 +1624,26 @@
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="accent_color">Accent Color</string>
|
||||
<string name="accent_color_description">Main color used across buttons and links</string>
|
||||
<string name="accent_color_purple">Purple</string>
|
||||
<string name="accent_color_blue">Blue</string>
|
||||
<string name="accent_color_green">Green</string>
|
||||
<string name="accent_color_orange">Orange</string>
|
||||
<string name="accent_color_red">Red</string>
|
||||
<string name="accent_color_pink">Pink</string>
|
||||
<string name="font_family">Font</string>
|
||||
<string name="font_family_description">Typeface used throughout the app</string>
|
||||
<string name="font_family_system">System Default</string>
|
||||
<string name="font_family_sans_serif">Sans Serif</string>
|
||||
<string name="font_family_serif">Serif</string>
|
||||
<string name="font_family_monospace">Monospace</string>
|
||||
<string name="font_size">Font Size</string>
|
||||
<string name="font_size_description">Scale the text size across the app</string>
|
||||
<string name="font_size_small">Small</string>
|
||||
<string name="font_size_normal">Normal</string>
|
||||
<string name="font_size_large">Large</string>
|
||||
<string name="font_size_huge">Huge</string>
|
||||
<string name="automatically_load_images_gifs">Image Preview</string>
|
||||
<string name="automatically_play_videos">Video Playback</string>
|
||||
<string name="autoplay_videos">Autoplay Videos</string>
|
||||
|
||||
Reference in New Issue
Block a user