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:
Vitor Pamplona
2026-06-27 16:00:11 -04:00
committed by GitHub
9 changed files with 361 additions and 59 deletions
@@ -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),
)
}
}
@@ -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
@@ -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,
@@ -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
+20
View File
@@ -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>