feat: theme colour overrides

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-31 23:52:16 +01:00
parent 9bbc8f513c
commit 35c8976e30
10 changed files with 453 additions and 41 deletions

View File

@ -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

View File

@ -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)

View File

@ -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(),

View File

@ -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 ->

View File

@ -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 ->

View File

@ -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<String, Int>? = null,
)

View File

@ -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,

View File

@ -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<String?>(null)
var selectedOverrideInitialValue by mutableStateOf<Int?>(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()
}

View File

@ -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<String, Int>? = 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<String, Int>?,
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,
)
}

View File

@ -421,6 +421,41 @@
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
<string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string>
<string name="settings_appearance_colour_overrides">Colour overrides</string>
<string name="settings_appearance_colour_overrides_primary">Primary</string>
<string name="settings_appearance_colour_overrides_on_primary">On Primary</string>
<string name="settings_appearance_colour_overrides_primary_container">Primary Container</string>
<string name="settings_appearance_colour_overrides_on_primary_container">On Primary Container</string>
<string name="settings_appearance_colour_overrides_inverse_primary">Inverse Primary</string>
<string name="settings_appearance_colour_overrides_secondary">Secondary</string>
<string name="settings_appearance_colour_overrides_on_secondary">On Secondary</string>
<string name="settings_appearance_colour_overrides_secondary_container">Secondary Container</string>
<string name="settings_appearance_colour_overrides_on_secondary_container">On Secondary Container</string>
<string name="settings_appearance_colour_overrides_tertiary">Tertiary</string>
<string name="settings_appearance_colour_overrides_on_tertiary">On Tertiary</string>
<string name="settings_appearance_colour_overrides_tertiary_container">Tertiary Container</string>
<string name="settings_appearance_colour_overrides_on_tertiary_container">On Tertiary Container</string>
<string name="settings_appearance_colour_overrides_background">Background</string>
<string name="settings_appearance_colour_overrides_on_background">On Background</string>
<string name="settings_appearance_colour_overrides_surface">Surface</string>
<string name="settings_appearance_colour_overrides_on_surface">On Surface</string>
<string name="settings_appearance_colour_overrides_surface_variant">Surface Variant</string>
<string name="settings_appearance_colour_overrides_on_surface_variant">On Surface Variant</string>
<string name="settings_appearance_colour_overrides_surface_tint">Surface Tint</string>
<string name="settings_appearance_colour_overrides_inverse_surface">Inverse Surface</string>
<string name="settings_appearance_colour_overrides_inverse_on_surface">Inverse On Surface</string>
<string name="settings_appearance_colour_overrides_error">Error</string>
<string name="settings_appearance_colour_overrides_on_error">On Error</string>
<string name="settings_appearance_colour_overrides_error_container">Error Container</string>
<string name="settings_appearance_colour_overrides_on_error_container">On Error Container</string>
<string name="settings_appearance_colour_overrides_outline">Outline</string>
<string name="settings_appearance_colour_overrides_outline_variant">Outline Variant</string>
<string name="settings_appearance_colour_overrides_scrim">Scrim</string>
<string name="settings_appearance_colour_overrides_apply">Apply</string>
<string name="settings_appearance_colour_overrides_reset">Reset</string>
<string name="settings_appearance_colour_overrides_export">Export</string>
<string name="settings_appearance_colour_overrides_import">Import</string>
<string name="settings_feedback">Feedback</string>
<string name="settings_feedback_introduction">Any feedback you have for Revolt is greatly appreciated and all feedback is read by the development team of our Android app.</string>
<string name="settings_feedback_category">Category</string>