feat: theme colour overrides
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
9bbc8f513c
commit
35c8976e30
|
|
@ -168,6 +168,7 @@ sentry {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Android/Kotlin Core
|
// Android/Kotlin Core
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
implementation 'androidx.core:core-ktx:1.10.1'
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.10"
|
||||||
|
|
||||||
// Kotlinx - various first-party extensions for Kotlin
|
// Kotlinx - various first-party extensions for Kotlin
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
|
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-datasource-okhttp:$media3_version"
|
||||||
implementation "androidx.media3:media3-ui:$media3_version"
|
implementation "androidx.media3:media3-ui:$media3_version"
|
||||||
|
|
||||||
|
// Colour picker
|
||||||
|
implementation "com.github.skydoves:colorpicker-compose:1.0.5"
|
||||||
|
|
||||||
// Debug-only dependencies
|
// Debug-only dependencies
|
||||||
|
|
||||||
// LeakCanary - memory leak detection
|
// LeakCanary - memory leak detection
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import chat.revolt.api.schemas.Invite
|
||||||
import chat.revolt.api.schemas.InviteJoined
|
import chat.revolt.api.schemas.InviteJoined
|
||||||
import chat.revolt.api.schemas.RsResult
|
import chat.revolt.api.schemas.RsResult
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
import chat.revolt.components.generic.IconPlaceholder
|
import chat.revolt.components.generic.IconPlaceholder
|
||||||
import chat.revolt.components.generic.RemoteImage
|
import chat.revolt.components.generic.RemoteImage
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
|
|
@ -143,7 +144,10 @@ fun InviteScreen(
|
||||||
val inviteValid = if (viewModel.loadingFinished) (viewModel.inviteResult?.ok ?: false) else null
|
val inviteValid = if (viewModel.loadingFinished) (viewModel.inviteResult?.ok ?: false) else null
|
||||||
val invite = viewModel.inviteResult?.value
|
val invite = viewModel.inviteResult?.value
|
||||||
|
|
||||||
RevoltTheme(requestedTheme = GlobalState.theme) {
|
RevoltTheme(
|
||||||
|
requestedTheme = GlobalState.theme,
|
||||||
|
colourOverrides = SyncedSettings.android.colourOverrides
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.navigation.compose.dialog
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import chat.revolt.BuildConfig
|
import chat.revolt.BuildConfig
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
import chat.revolt.ndk.NativeLibraries
|
import chat.revolt.ndk.NativeLibraries
|
||||||
import chat.revolt.screens.SplashScreen
|
import chat.revolt.screens.SplashScreen
|
||||||
import chat.revolt.screens.about.AboutScreen
|
import chat.revolt.screens.about.AboutScreen
|
||||||
|
|
@ -86,7 +87,8 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
RevoltTheme(
|
RevoltTheme(
|
||||||
requestedTheme = GlobalState.theme
|
requestedTheme = GlobalState.theme,
|
||||||
|
colourOverrides = SyncedSettings.android.colourOverrides
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import chat.revolt.api.REVOLT_FILES
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
import chat.revolt.api.schemas.AutumnResource
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
import chat.revolt.components.generic.PageHeader
|
import chat.revolt.components.generic.PageHeader
|
||||||
import chat.revolt.provider.getAttachmentContentUri
|
import chat.revolt.provider.getAttachmentContentUri
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
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(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
) { pv ->
|
) { pv ->
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import chat.revolt.api.REVOLT_FILES
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
import chat.revolt.api.schemas.AutumnResource
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
import chat.revolt.components.generic.PageHeader
|
import chat.revolt.components.generic.PageHeader
|
||||||
import chat.revolt.provider.getAttachmentContentUri
|
import chat.revolt.provider.getAttachmentContentUri
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
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(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
) { pv ->
|
) { pv ->
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,10 @@ data class AndroidSpecificSettings(
|
||||||
* The theme to use for the app.
|
* The theme to use for the app.
|
||||||
* Can be one of `{ None, Revolt, Light, M3Dynamic, Amoled }`
|
* 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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,16 @@ package chat.revolt.components.screens.settings.appearance
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -13,14 +19,14 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThemeChip(
|
fun ColourChip(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
color: Color,
|
color: Color,
|
||||||
text: String,
|
text: String,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
|
|
@ -33,19 +39,21 @@ fun ThemeChip(
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(4.dp)
|
.padding(8.dp),
|
||||||
.padding(8.dp)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(color)
|
.background(color)
|
||||||
.height(60.dp)
|
.height(48.dp)
|
||||||
.fillMaxWidth(1f)
|
.width(48.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
style = MaterialTheme.typography.labelLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +62,7 @@ fun ThemeChip(
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedThemeChipPreview() {
|
fun SelectedThemeChipPreview() {
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
text = "Red",
|
text = "Red",
|
||||||
selected = true,
|
selected = true,
|
||||||
|
|
@ -1,25 +1,58 @@
|
||||||
package chat.revolt.screens.settings
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
import android.widget.Toast
|
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.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -29,32 +62,107 @@ import chat.revolt.R
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
import chat.revolt.api.settings.SyncedSettings
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
import chat.revolt.components.generic.PageHeader
|
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.Theme
|
||||||
import chat.revolt.ui.theme.systemSupportsDynamicColors
|
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 kotlinx.coroutines.launch
|
||||||
|
import kotlin.reflect.KVisibility
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
class AppearanceSettingsScreenViewModel : ViewModel() {
|
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) {
|
fun saveNewTheme(theme: Theme) {
|
||||||
|
GlobalState.theme = theme
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val android = SyncedSettings.android
|
SyncedSettings.updateAndroid(SyncedSettings.android.copy(theme = theme.name))
|
||||||
android.theme = theme.toString()
|
}
|
||||||
SyncedSettings.updateAndroid(android)
|
}
|
||||||
|
|
||||||
|
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
|
@Composable
|
||||||
fun AppearanceSettingsScreen(
|
fun AppearanceSettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: AppearanceSettingsScreenViewModel = viewModel()
|
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) {
|
val context = LocalContext.current
|
||||||
GlobalState.theme = theme
|
val scope = rememberCoroutineScope()
|
||||||
viewModel.saveNewTheme(theme)
|
|
||||||
|
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(
|
Column(
|
||||||
|
|
@ -74,19 +182,21 @@ fun AppearanceSettingsScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.settings_appearance_theme),
|
text = stringResource(id = R.string.settings_appearance_theme),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.padding(bottom = 10.dp)
|
modifier = Modifier.padding(start = 20.dp, end = 20.dp, bottom = 10.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 20.dp, end = 20.dp)
|
||||||
) {
|
) {
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = Color(0xff1c243c),
|
color = Color(0xff1c243c),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_revolt),
|
text = stringResource(id = R.string.settings_appearance_theme_revolt),
|
||||||
selected = GlobalState.theme == Theme.Revolt,
|
selected = GlobalState.theme == Theme.Revolt,
|
||||||
|
|
@ -94,10 +204,10 @@ fun AppearanceSettingsScreen(
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("set_theme_revolt")
|
.testTag("set_theme_revolt")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.Revolt)
|
viewModel.saveNewTheme(Theme.Revolt)
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = Color(0xfff7f7f7),
|
color = Color(0xfff7f7f7),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_light),
|
text = stringResource(id = R.string.settings_appearance_theme_light),
|
||||||
selected = GlobalState.theme == Theme.Light,
|
selected = GlobalState.theme == Theme.Light,
|
||||||
|
|
@ -105,10 +215,10 @@ fun AppearanceSettingsScreen(
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("set_theme_light")
|
.testTag("set_theme_light")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.Light)
|
viewModel.saveNewTheme(Theme.Light)
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = Color(0xff000000),
|
color = Color(0xff000000),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_amoled),
|
text = stringResource(id = R.string.settings_appearance_theme_amoled),
|
||||||
selected = GlobalState.theme == Theme.Amoled,
|
selected = GlobalState.theme == Theme.Amoled,
|
||||||
|
|
@ -116,10 +226,10 @@ fun AppearanceSettingsScreen(
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("set_theme_amoled")
|
.testTag("set_theme_amoled")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.Amoled)
|
viewModel.saveNewTheme(Theme.Amoled)
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = if (isSystemInDarkTheme()) Color(0xff1c243c) else Color(0xfff7f7f7),
|
color = if (isSystemInDarkTheme()) Color(0xff1c243c) else Color(0xfff7f7f7),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_none),
|
text = stringResource(id = R.string.settings_appearance_theme_none),
|
||||||
selected = GlobalState.theme == Theme.None,
|
selected = GlobalState.theme == Theme.None,
|
||||||
|
|
@ -127,11 +237,11 @@ fun AppearanceSettingsScreen(
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("set_theme_none")
|
.testTag("set_theme_none")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.None)
|
viewModel.saveNewTheme(Theme.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (systemSupportsDynamicColors()) {
|
if (systemSupportsDynamicColors()) {
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = dynamicDarkColorScheme(LocalContext.current).primary,
|
color = dynamicDarkColorScheme(LocalContext.current).primary,
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_m3dynamic),
|
text = stringResource(id = R.string.settings_appearance_theme_m3dynamic),
|
||||||
selected = GlobalState.theme == Theme.M3Dynamic,
|
selected = GlobalState.theme == Theme.M3Dynamic,
|
||||||
|
|
@ -139,10 +249,10 @@ fun AppearanceSettingsScreen(
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("set_theme_m3dynamic")
|
.testTag("set_theme_m3dynamic")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.M3Dynamic)
|
viewModel.saveNewTheme(Theme.M3Dynamic)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ThemeChip(
|
ColourChip(
|
||||||
color = Color(0xffa0a0a0),
|
color = Color(0xffa0a0a0),
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
id = R.string.settings_appearance_theme_m3dynamic_unsupported
|
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()
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,16 @@ package chat.revolt.ui.theme
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
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.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.graphics.Color
|
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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
import kotlin.reflect.KMutableProperty
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
val RevoltColorScheme = darkColorScheme(
|
val RevoltColorScheme = darkColorScheme(
|
||||||
primary = Color(0xffda4e5b),
|
primary = Color(0xffda4e5b),
|
||||||
|
|
@ -58,9 +68,8 @@ enum class Theme {
|
||||||
Amoled
|
Amoled
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RevoltTheme(requestedTheme: Theme, content: @Composable () -> Unit) {
|
fun getColorScheme(requestedTheme: Theme, colourOverrides: Map<String, Int>? = null): ColorScheme {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val systemInDarkTheme = isSystemInDarkTheme()
|
val systemInDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
@ -81,7 +90,7 @@ fun RevoltTheme(requestedTheme: Theme, content: @Composable () -> Unit) {
|
||||||
requestedTheme == Theme.None && systemInDarkTheme -> RevoltColorScheme
|
requestedTheme == Theme.None && systemInDarkTheme -> RevoltColorScheme
|
||||||
requestedTheme == Theme.None && !systemInDarkTheme -> LightColorScheme
|
requestedTheme == Theme.None && !systemInDarkTheme -> LightColorScheme
|
||||||
else -> RevoltColorScheme
|
else -> RevoltColorScheme
|
||||||
}
|
}.copy()
|
||||||
|
|
||||||
val colorSchemeIsDark = when {
|
val colorSchemeIsDark = when {
|
||||||
m3Supported && requestedTheme == Theme.M3Dynamic -> isSystemInDarkTheme()
|
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(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = RevoltTypography,
|
typography = RevoltTypography,
|
||||||
|
|
@ -121,3 +153,16 @@ fun getDefaultTheme(): Theme {
|
||||||
else -> Theme.Revolt
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -421,6 +421,41 @@
|
||||||
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
|
<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_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">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_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>
|
<string name="settings_feedback_category">Category</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue