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 {
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue