From 35c8976e30df0e5bd3119211b5001b3563d0acbc Mon Sep 17 00:00:00 2001 From: Infi Date: Tue, 31 Oct 2023 23:52:16 +0100 Subject: [PATCH] feat: theme colour overrides Signed-off-by: Infi --- app/build.gradle | 4 + .../chat/revolt/activities/InviteActivity.kt | 6 +- .../chat/revolt/activities/MainActivity.kt | 4 +- .../activities/media/ImageViewActivity.kt | 6 +- .../activities/media/VideoViewActivity.kt | 6 +- .../java/chat/revolt/api/schemas/Settings.kt | 7 +- .../{ThemeChip.kt => ColourChip.kt} | 26 +- .../settings/AppearanceSettingsScreen.kt | 347 ++++++++++++++++-- .../main/java/chat/revolt/ui/theme/Theme.kt | 53 ++- app/src/main/res/values/strings.xml | 35 ++ 10 files changed, 453 insertions(+), 41 deletions(-) rename app/src/main/java/chat/revolt/components/screens/settings/appearance/{ThemeChip.kt => ColourChip.kt} (72%) diff --git a/app/build.gradle b/app/build.gradle index 43e0a457..4f7a482a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -168,6 +168,7 @@ sentry { dependencies { // Android/Kotlin Core implementation 'androidx.core:core-ktx:1.10.1' + implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.10" // Kotlinx - various first-party extensions for Kotlin implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" @@ -254,6 +255,9 @@ dependencies { implementation "androidx.media3:media3-datasource-okhttp:$media3_version" implementation "androidx.media3:media3-ui:$media3_version" + // Colour picker + implementation "com.github.skydoves:colorpicker-compose:1.0.5" + // Debug-only dependencies // LeakCanary - memory leak detection diff --git a/app/src/main/java/chat/revolt/activities/InviteActivity.kt b/app/src/main/java/chat/revolt/activities/InviteActivity.kt index 94a9c1b0..15ef797d 100644 --- a/app/src/main/java/chat/revolt/activities/InviteActivity.kt +++ b/app/src/main/java/chat/revolt/activities/InviteActivity.kt @@ -56,6 +56,7 @@ import chat.revolt.api.schemas.Invite import chat.revolt.api.schemas.InviteJoined import chat.revolt.api.schemas.RsResult import chat.revolt.api.settings.GlobalState +import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.generic.IconPlaceholder import chat.revolt.components.generic.RemoteImage import chat.revolt.ui.theme.RevoltTheme @@ -143,7 +144,10 @@ fun InviteScreen( val inviteValid = if (viewModel.loadingFinished) (viewModel.inviteResult?.ok ?: false) else null val invite = viewModel.inviteResult?.value - RevoltTheme(requestedTheme = GlobalState.theme) { + RevoltTheme( + requestedTheme = GlobalState.theme, + colourOverrides = SyncedSettings.android.colourOverrides + ) { Surface( modifier = Modifier .background(MaterialTheme.colorScheme.background) diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index 44c3ec3a..ca6c8b8b 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController import chat.revolt.BuildConfig import chat.revolt.api.settings.GlobalState +import chat.revolt.api.settings.SyncedSettings import chat.revolt.ndk.NativeLibraries import chat.revolt.screens.SplashScreen import chat.revolt.screens.about.AboutScreen @@ -86,7 +87,8 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) { val navController = rememberNavController() RevoltTheme( - requestedTheme = GlobalState.theme + requestedTheme = GlobalState.theme, + colourOverrides = SyncedSettings.android.colourOverrides ) { Surface( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/chat/revolt/activities/media/ImageViewActivity.kt b/app/src/main/java/chat/revolt/activities/media/ImageViewActivity.kt index e3f29b9c..302cefbc 100644 --- a/app/src/main/java/chat/revolt/activities/media/ImageViewActivity.kt +++ b/app/src/main/java/chat/revolt/activities/media/ImageViewActivity.kt @@ -45,6 +45,7 @@ import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltHttp import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.settings.GlobalState +import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.generic.PageHeader import chat.revolt.provider.getAttachmentContentUri import chat.revolt.ui.theme.RevoltTheme @@ -177,7 +178,10 @@ fun ImageViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) { } } - RevoltTheme(requestedTheme = GlobalState.theme) { + RevoltTheme( + requestedTheme = GlobalState.theme, + colourOverrides = SyncedSettings.android.colourOverrides + ) { Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { pv -> diff --git a/app/src/main/java/chat/revolt/activities/media/VideoViewActivity.kt b/app/src/main/java/chat/revolt/activities/media/VideoViewActivity.kt index 41c7b693..f165c9fd 100644 --- a/app/src/main/java/chat/revolt/activities/media/VideoViewActivity.kt +++ b/app/src/main/java/chat/revolt/activities/media/VideoViewActivity.kt @@ -49,6 +49,7 @@ import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltHttp import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.settings.GlobalState +import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.generic.PageHeader import chat.revolt.provider.getAttachmentContentUri import chat.revolt.ui.theme.RevoltTheme @@ -193,7 +194,10 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) { } } - RevoltTheme(requestedTheme = GlobalState.theme) { + RevoltTheme( + requestedTheme = GlobalState.theme, + colourOverrides = SyncedSettings.android.colourOverrides + ) { Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { pv -> diff --git a/app/src/main/java/chat/revolt/api/schemas/Settings.kt b/app/src/main/java/chat/revolt/api/schemas/Settings.kt index 2d8014e1..7db00146 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Settings.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Settings.kt @@ -13,5 +13,10 @@ data class AndroidSpecificSettings( * The theme to use for the app. * Can be one of `{ None, Revolt, Light, M3Dynamic, Amoled }` */ - var theme: String? = null + var theme: String? = null, + /** + * Colour overrides. + * Map of `primary, onPrimary, primaryContainer, onPrimaryContainer, inversePrimary, secondary, onSecondary, secondaryContainer, onSecondaryContainer, tertiary, onTertiary, tertiaryContainer, onTertiaryContainer, background, onBackground, surface, onSurface, surfaceVariant, onSurfaceVariant, surfaceTint, inverseSurface, inverseOnSurface, error, onError, errorContainer, onErrorContainer, outline, outlineVariant, scrim` to int colours. + */ + var colourOverrides: Map? = null, ) diff --git a/app/src/main/java/chat/revolt/components/screens/settings/appearance/ThemeChip.kt b/app/src/main/java/chat/revolt/components/screens/settings/appearance/ColourChip.kt similarity index 72% rename from app/src/main/java/chat/revolt/components/screens/settings/appearance/ThemeChip.kt rename to app/src/main/java/chat/revolt/components/screens/settings/appearance/ColourChip.kt index c085a6b6..33ca844c 100644 --- a/app/src/main/java/chat/revolt/components/screens/settings/appearance/ThemeChip.kt +++ b/app/src/main/java/chat/revolt/components/screens/settings/appearance/ColourChip.kt @@ -2,10 +2,16 @@ package chat.revolt.components.screens.settings.appearance import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -13,14 +19,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable -fun ThemeChip( +fun ColourChip( modifier: Modifier = Modifier, color: Color, text: String, selected: Boolean = false, onClick: () -> Unit ) { - Column( + Row( Modifier .clip(MaterialTheme.shapes.medium) .clickable(onClick = onClick) @@ -33,19 +39,21 @@ fun ThemeChip( Modifier } ) - .padding(4.dp) - .padding(8.dp) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .clip(MaterialTheme.shapes.medium) .background(color) - .height(60.dp) - .fillMaxWidth(1f) + .height(48.dp) + .width(48.dp) ) + + Spacer(Modifier.width(16.dp)) + Text( text = text, - modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.labelLarge ) } @@ -54,7 +62,7 @@ fun ThemeChip( @Preview @Composable fun SelectedThemeChipPreview() { - ThemeChip( + ColourChip( color = Color.Red, text = "Red", selected = true, diff --git a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt index 00df9ceb..e8b8656a 100644 --- a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt @@ -1,25 +1,58 @@ package chat.revolt.screens.settings import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.ripple.LocalRippleTheme +import androidx.compose.material3.Button +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -29,32 +62,107 @@ import chat.revolt.R import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.generic.PageHeader -import chat.revolt.components.screens.settings.appearance.ThemeChip +import chat.revolt.components.screens.settings.appearance.ColourChip +import chat.revolt.ui.theme.ClearRippleTheme import chat.revolt.ui.theme.Theme import chat.revolt.ui.theme.systemSupportsDynamicColors +import com.github.skydoves.colorpicker.compose.AlphaSlider +import com.github.skydoves.colorpicker.compose.BrightnessSlider +import com.github.skydoves.colorpicker.compose.ColorEnvelope +import com.github.skydoves.colorpicker.compose.HsvColorPicker +import com.github.skydoves.colorpicker.compose.rememberColorPickerController import kotlinx.coroutines.launch +import kotlin.reflect.KVisibility +import kotlin.reflect.full.memberProperties class AppearanceSettingsScreenViewModel : ViewModel() { + var showColourOverrides by mutableStateOf(false) + var selectedOverrideName by mutableStateOf(null) + var selectedOverrideInitialValue by mutableStateOf(null) + var overridePickerSheetVisible by mutableStateOf(false) + fun saveNewTheme(theme: Theme) { + GlobalState.theme = theme viewModelScope.launch { - val android = SyncedSettings.android - android.theme = theme.toString() - SyncedSettings.updateAndroid(android) + SyncedSettings.updateAndroid(SyncedSettings.android.copy(theme = theme.name)) + } + } + + fun updateColourOverrides(fieldName: String, value: Int?) { + viewModelScope.launch { + val overrides = SyncedSettings.android.copy().colourOverrides + + if (overrides != null) { + val mutOverrides = overrides.toMutableMap() + if (value == null) { + mutOverrides.remove(fieldName) + } else { + mutOverrides[fieldName] = value + } + + SyncedSettings.updateAndroid( + SyncedSettings.android.copy( + colourOverrides = mutOverrides + ) + ) + } else if (value != null) { + SyncedSettings.updateAndroid( + SyncedSettings.android.copy( + colourOverrides = mapOf( + fieldName to value + ) + ) + ) + } } } } -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun AppearanceSettingsScreen( navController: NavController, viewModel: AppearanceSettingsScreenViewModel = viewModel() ) { - val context = LocalContext.current + val colourOverridesOpenerArrowRotation by animateFloatAsState( + if (viewModel.showColourOverrides) { + if (LocalLayoutDirection.current == LayoutDirection.Ltr) 90f else -90f + } else 0f, + label = "colourOverridesOpenerArrowRotation" + ) - fun setNewTheme(theme: Theme) { - GlobalState.theme = theme - viewModel.saveNewTheme(theme) + val context = LocalContext.current + val scope = rememberCoroutineScope() + + if (viewModel.overridePickerSheetVisible) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + viewModel.overridePickerSheetVisible = false + } + ) { + ColourSelectorSheet( + initialValue = Color(viewModel.selectedOverrideInitialValue ?: 0), + onConfirm = { color -> + viewModel.updateColourOverrides( + viewModel.selectedOverrideName ?: return@ColourSelectorSheet, + color?.toArgb() + ) + scope.launch { + sheetState.hide() + viewModel.overridePickerSheetVisible = false + } + }, + onDismiss = { + scope.launch { + sheetState.hide() + viewModel.overridePickerSheetVisible = false + } + } + ) + } } Column( @@ -74,19 +182,21 @@ fun AppearanceSettingsScreen( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(20.dp) ) { Text( text = stringResource(id = R.string.settings_appearance_theme), - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.padding(bottom = 10.dp) + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(start = 20.dp, end = 20.dp, bottom = 10.dp) ) FlowRow( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp) ) { - ThemeChip( + ColourChip( color = Color(0xff1c243c), text = stringResource(id = R.string.settings_appearance_theme_revolt), selected = GlobalState.theme == Theme.Revolt, @@ -94,10 +204,10 @@ fun AppearanceSettingsScreen( .weight(1f) .testTag("set_theme_revolt") ) { - setNewTheme(Theme.Revolt) + viewModel.saveNewTheme(Theme.Revolt) } - ThemeChip( + ColourChip( color = Color(0xfff7f7f7), text = stringResource(id = R.string.settings_appearance_theme_light), selected = GlobalState.theme == Theme.Light, @@ -105,10 +215,10 @@ fun AppearanceSettingsScreen( .weight(1f) .testTag("set_theme_light") ) { - setNewTheme(Theme.Light) + viewModel.saveNewTheme(Theme.Light) } - ThemeChip( + ColourChip( color = Color(0xff000000), text = stringResource(id = R.string.settings_appearance_theme_amoled), selected = GlobalState.theme == Theme.Amoled, @@ -116,10 +226,10 @@ fun AppearanceSettingsScreen( .weight(1f) .testTag("set_theme_amoled") ) { - setNewTheme(Theme.Amoled) + viewModel.saveNewTheme(Theme.Amoled) } - ThemeChip( + ColourChip( color = if (isSystemInDarkTheme()) Color(0xff1c243c) else Color(0xfff7f7f7), text = stringResource(id = R.string.settings_appearance_theme_none), selected = GlobalState.theme == Theme.None, @@ -127,11 +237,11 @@ fun AppearanceSettingsScreen( .weight(1f) .testTag("set_theme_none") ) { - setNewTheme(Theme.None) + viewModel.saveNewTheme(Theme.None) } if (systemSupportsDynamicColors()) { - ThemeChip( + ColourChip( color = dynamicDarkColorScheme(LocalContext.current).primary, text = stringResource(id = R.string.settings_appearance_theme_m3dynamic), selected = GlobalState.theme == Theme.M3Dynamic, @@ -139,10 +249,10 @@ fun AppearanceSettingsScreen( .weight(1f) .testTag("set_theme_m3dynamic") ) { - setNewTheme(Theme.M3Dynamic) + viewModel.saveNewTheme(Theme.M3Dynamic) } } else { - ThemeChip( + ColourChip( color = Color(0xffa0a0a0), text = stringResource( id = R.string.settings_appearance_theme_m3dynamic_unsupported @@ -162,6 +272,197 @@ fun AppearanceSettingsScreen( } } } + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier + .clickable { + viewModel.showColourOverrides = !viewModel.showColourOverrides + } + .fillMaxWidth() + .padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (LocalLayoutDirection.current == LayoutDirection.Ltr) { + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier + .padding(start = 20.dp, end = 4.dp) + .rotate(colourOverridesOpenerArrowRotation) + ) + } + + Text( + text = stringResource(id = R.string.settings_appearance_colour_overrides), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .weight(1f) + ) + + if (LocalLayoutDirection.current == LayoutDirection.Rtl) { + Icon( + imageVector = Icons.Default.KeyboardArrowLeft, + contentDescription = null, + modifier = Modifier + .padding(start = 4.dp, end = 20.dp) + .rotate(colourOverridesOpenerArrowRotation) + ) + } + } + + AnimatedVisibility(viewModel.showColourOverrides) { + Column { + ColorScheme::class.memberProperties.forEach { member -> + if (member.visibility != KVisibility.PUBLIC) return@forEach + + val name = member.name + val value = member.getter.call(MaterialTheme.colorScheme) as Color + + ColourChip( + color = value, + text = try { + R.string::class.java.getField("settings_appearance_colour_overrides_${name.toSnakeCase()}") + .getInt(null) + .let { context.getString(it) } + } catch (e: Exception) { + name + }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp) + .testTag("set_colour_override_$name") + ) { + viewModel.selectedOverrideName = name + viewModel.selectedOverrideInitialValue = value.toArgb() + viewModel.overridePickerSheetVisible = true + } + } + } + } } } } + +@Composable +fun ColourSelectorSheet( + initialValue: Color, + onConfirm: (Color?) -> Unit, + onDismiss: () -> Unit +) { + val controller = rememberColorPickerController() + val colour = remember { mutableStateOf(initialValue) } + + Column( + modifier = Modifier + .padding(20.dp) + .verticalScroll(rememberScrollState()) + ) { + HsvColorPicker( + modifier = Modifier + .fillMaxWidth() + .height(450.dp) + .padding(10.dp), + controller = controller, + onColorChanged = { colorEnvelope: ColorEnvelope -> + colour.value = colorEnvelope.color + }, + ) + + AlphaSlider( + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + controller = controller, + ) + + BrightnessSlider( + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + controller = controller, + ) + + CompositionLocalProvider( + LocalRippleTheme provides ClearRippleTheme + ) { + ColourChip( + color = colour.value, + text = "#${ + (0xFFFFFF and colour.value.toArgb()).toString(16).padStart(6, '0').uppercase() + }", + modifier = Modifier + .padding(10.dp) + .fillMaxWidth() + ) {} + } + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextButton( + onClick = { + onConfirm(null) + }, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(id = R.string.settings_appearance_colour_overrides_reset) + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton( + onClick = { + onDismiss() + }, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(id = R.string.cancel) + ) + } + + Button( + onClick = { + onConfirm(colour.value) + }, + enabled = colour.value != initialValue, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(id = R.string.settings_appearance_colour_overrides_apply) + ) + } + } + } + } +} + +fun String.toSnakeCase(): String { + return this.replace(Regex("([a-z])([A-Z]+)"), "$1_$2").lowercase() +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/ui/theme/Theme.kt b/app/src/main/java/chat/revolt/ui/theme/Theme.kt index da054969..267970e4 100644 --- a/app/src/main/java/chat/revolt/ui/theme/Theme.kt +++ b/app/src/main/java/chat/revolt/ui/theme/Theme.kt @@ -3,8 +3,16 @@ package chat.revolt.ui.theme import android.annotation.SuppressLint import android.app.Activity import android.os.Build +import android.util.Log import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.* +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material.ripple.RippleTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color @@ -12,6 +20,8 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.ViewCompat +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.memberProperties val RevoltColorScheme = darkColorScheme( primary = Color(0xffda4e5b), @@ -58,9 +68,8 @@ enum class Theme { Amoled } -@SuppressLint("NewApi") @Composable -fun RevoltTheme(requestedTheme: Theme, content: @Composable () -> Unit) { +fun getColorScheme(requestedTheme: Theme, colourOverrides: Map? = null): ColorScheme { val context = LocalContext.current val systemInDarkTheme = isSystemInDarkTheme() @@ -81,7 +90,7 @@ fun RevoltTheme(requestedTheme: Theme, content: @Composable () -> Unit) { requestedTheme == Theme.None && systemInDarkTheme -> RevoltColorScheme requestedTheme == Theme.None && !systemInDarkTheme -> LightColorScheme else -> RevoltColorScheme - } + }.copy() val colorSchemeIsDark = when { m3Supported && requestedTheme == Theme.M3Dynamic -> isSystemInDarkTheme() @@ -104,6 +113,29 @@ fun RevoltTheme(requestedTheme: Theme, content: @Composable () -> Unit) { } } + colorScheme::class.memberProperties.forEach { + if (it is KMutableProperty<*>) { + val name = it.name + val value = colourOverrides?.get(name) + if (value != null) { + Log.d("RevoltTheme", "Overriding $name with $value") + it.setter.call(colorScheme, Color(value)) + } + } + } + + return colorScheme +} + +@SuppressLint("NewApi") +@Composable +fun RevoltTheme( + requestedTheme: Theme, + colourOverrides: Map?, + content: @Composable () -> Unit +) { + val colorScheme = getColorScheme(requestedTheme, colourOverrides) + MaterialTheme( colorScheme = colorScheme, typography = RevoltTypography, @@ -121,3 +153,16 @@ fun getDefaultTheme(): Theme { else -> Theme.Revolt } } + +object ClearRippleTheme : RippleTheme { + @Composable + override fun defaultColor(): Color = Color.Transparent + + @Composable + override fun rippleAlpha() = RippleAlpha( + draggedAlpha = 0.0f, + focusedAlpha = 0.0f, + hoveredAlpha = 0.0f, + pressedAlpha = 0.0f, + ) +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 684b6d45..2ee75e90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -421,6 +421,41 @@ Material You (unsupported) Material You is not supported on this device. + Colour overrides + Primary + On Primary + Primary Container + On Primary Container + Inverse Primary + Secondary + On Secondary + Secondary Container + On Secondary Container + Tertiary + On Tertiary + Tertiary Container + On Tertiary Container + Background + On Background + Surface + On Surface + Surface Variant + On Surface Variant + Surface Tint + Inverse Surface + Inverse On Surface + Error + On Error + Error Container + On Error Container + Outline + Outline Variant + Scrim + Apply + Reset + Export + Import + Feedback Any feedback you have for Revolt is greatly appreciated and all feedback is read by the development team of our Android app. Category