diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index 069c8a8805..e986f5f778 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index 176691dfe6..d86fc45a51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -54,6 +54,9 @@ class UiSettingsFlow( val showProfileFollowersFeed: MutableStateFlow = MutableStateFlow(true), val dontShowOnchainPublicWarning: MutableStateFlow = MutableStateFlow(false), val suggestWorkoutsFromHealthConnect: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), + val accentColor: MutableStateFlow = MutableStateFlow(AccentColorType.PURPLE), + val fontFamily: MutableStateFlow = MutableStateFlow(FontFamilyType.SYSTEM), + val fontSize: MutableStateFlow = MutableStateFlow(FontSizeType.NORMAL), ) { val listOfFlows: List> = listOf>( @@ -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), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index af4c15f520..25f5ba21cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 0d3a39e992..9a3f6839da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt index 5b6ffb4fe2..c39794a696 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index 98c40b26bc..6ca1574e84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 213d04d4e6..39f561d15d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt index b36b940d4f..88aa8e5178 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt @@ -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 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9446093496..fde413a38a 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1624,6 +1624,26 @@ Wallet Connect Language Theme + Accent Color + Main color used across buttons and links + Purple + Blue + Green + Orange + Red + Pink + Font + Typeface used throughout the app + System Default + Sans Serif + Serif + Monospace + Font Size + Scale the text size across the app + Small + Normal + Large + Huge Image Preview Video Playback Autoplay Videos