diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8c0c19e2..01c24361 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -81,4 +81,30 @@ public static *** i(...); public static *** w(...); public static *** e(...); +} + +-dontwarn kotlin.** +-dontwarn org.w3c.dom.events.* +-dontwarn org.jetbrains.kotlin.di.InjectorForRuntimeDescriptorLoader + +-keep class kotlin.** { *; } +-keep class org.jetbrains.kotlin.** { *; } + +-keepclassmembers,allowoptimization enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); + **[] $VALUES; + public *; +} + +-keepattributes InnerClasses + +-keep class androidx.compose.ui.graphics.ColorKt { *; } + +-keep class androidx.compose.material3.ColorScheme { *; } +-keep class androidx.compose.material3.ColorSchemeKt { *; } +-keep class androidx.compose.material3.ColorSchemeKt$* { *; } +-keepclassmembers class androidx.compose.material3.ColorSchemeKt { + public static final ; + public ; } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Settings.kt b/app/src/main/java/chat/revolt/api/schemas/Settings.kt index 7db00146..b465259f 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Settings.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Settings.kt @@ -1,5 +1,6 @@ package chat.revolt.api.schemas +import chat.revolt.ui.theme.OverridableColourScheme import kotlinx.serialization.Serializable @Serializable @@ -18,5 +19,5 @@ data class AndroidSpecificSettings( * Colour overrides. * Map of `primary, onPrimary, primaryContainer, onPrimaryContainer, inversePrimary, secondary, onSecondary, secondaryContainer, onSecondaryContainer, tertiary, onTertiary, tertiaryContainer, onTertiaryContainer, background, onBackground, surface, onSurface, surfaceVariant, onSurfaceVariant, surfaceTint, inverseSurface, inverseOnSurface, error, onError, errorContainer, onErrorContainer, outline, outlineVariant, scrim` to int colours. */ - var colourOverrides: Map? = null, + var colourOverrides: OverridableColourScheme? = null, ) diff --git a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt index a289e091..dde3a68a 100644 --- a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt @@ -31,7 +31,6 @@ 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 @@ -65,12 +64,15 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.RevoltCbor +import chat.revolt.api.RevoltJson 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.ColourChip import chat.revolt.ui.theme.ClearRippleTheme +import chat.revolt.ui.theme.OverridableColourScheme import chat.revolt.ui.theme.Theme +import chat.revolt.ui.theme.getFieldByName import chat.revolt.ui.theme.systemSupportsDynamicColors import com.github.skydoves.colorpicker.compose.AlphaSlider import com.github.skydoves.colorpicker.compose.BrightnessSlider @@ -85,8 +87,6 @@ import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer import java.io.File import javax.inject.Inject -import kotlin.reflect.KVisibility -import kotlin.reflect.full.memberProperties @HiltViewModel @Suppress("StaticFieldLeak") @@ -110,7 +110,17 @@ class AppearanceSettingsScreenViewModel @Inject constructor( val overrides = SyncedSettings.android.copy().colourOverrides if (overrides != null) { - val mutOverrides = overrides.toMutableMap() + // Yes, this looks stupid. Please see the comments in OverridableColourScheme.kt regarding this. + val json = RevoltJson.encodeToString( + OverridableColourScheme.serializer(), + overrides + ) + val asMap = RevoltJson.decodeFromString( + MapSerializer(String.serializer(), Int.serializer()), + json + ) + + val mutOverrides = asMap.toMutableMap() if (value == null) { mutOverrides.remove(fieldName) } else { @@ -119,35 +129,31 @@ class AppearanceSettingsScreenViewModel @Inject constructor( SyncedSettings.updateAndroid( SyncedSettings.android.copy( - colourOverrides = mutOverrides + colourOverrides = OverridableColourScheme() + .applyFromKeyValueMap(mutOverrides) ) ) } else if (value != null) { SyncedSettings.updateAndroid( SyncedSettings.android.copy( - colourOverrides = mapOf( - fieldName to value - ) + colourOverrides = OverridableColourScheme() + .applyFromKeyValueMap( + mapOf(fieldName to value) + ) ) ) } } } - private fun validOverrideKey(key: String): Boolean { - return ColorScheme::class.memberProperties.any { it.name == key } - } - private fun applyBulkOverrides(overrides: Map) { - val existingOverrides = SyncedSettings.android.colourOverrides ?: mapOf() - val newOverrides = existingOverrides.toMutableMap() - - newOverrides.putAll(overrides.filterKeys { validOverrideKey(it) }) + val existingOverrides = SyncedSettings.android.colourOverrides ?: OverridableColourScheme() viewModelScope.launch { SyncedSettings.updateAndroid( SyncedSettings.android.copy( - colourOverrides = newOverrides + colourOverrides = existingOverrides + .applyFromKeyValueMap(overrides.filterKeys { it in OverridableColourScheme.fieldNames }) ) ) } @@ -186,8 +192,8 @@ class AppearanceSettingsScreenViewModel @Inject constructor( context.contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write( RevoltCbor.encodeToByteArray( - MapSerializer(String.serializer(), Int.serializer()), - SyncedSettings.android.colourOverrides ?: mapOf() + OverridableColourScheme.serializer(), + SyncedSettings.android.colourOverrides ?: OverridableColourScheme() ) ) } @@ -452,28 +458,23 @@ fun AppearanceSettingsScreen( Spacer(modifier = Modifier.height(10.dp)) - 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 + OverridableColourScheme.fieldNames.forEach { fieldName -> + val value = + SyncedSettings.android.colourOverrides?.getFieldByName(fieldName) + ?: MaterialTheme.colorScheme.getFieldByName(fieldName) 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 - }, + color = Color(value ?: 0), + text = OverridableColourScheme.fieldNameToResource[fieldName] + ?.let { context.getString(it) } + ?: fieldName, modifier = Modifier .fillMaxWidth() .padding(start = 20.dp, end = 20.dp) - .testTag("set_colour_override_$name") + .testTag("set_colour_override_$fieldName") ) { - viewModel.selectedOverrideName = name - viewModel.selectedOverrideInitialValue = value.toArgb() + viewModel.selectedOverrideName = fieldName + viewModel.selectedOverrideInitialValue = value viewModel.overridePickerSheetVisible = true } } diff --git a/app/src/main/java/chat/revolt/ui/theme/OverridableColourScheme.kt b/app/src/main/java/chat/revolt/ui/theme/OverridableColourScheme.kt new file mode 100644 index 00000000..dbfcdd86 --- /dev/null +++ b/app/src/main/java/chat/revolt/ui/theme/OverridableColourScheme.kt @@ -0,0 +1,286 @@ +package chat.revolt.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import chat.revolt.R +import kotlinx.serialization.Serializable + +// Word of warning, this file is ugly, because I've had to fight a bit with the Compose compiler, +// namely native Kotlin (not Java) reflection seems to consistently break it. +// So I've had to resort to... methods like this. I'm sorry. +// If you've been linked to this file, I promise the rest of the codebase is not like this. +// Original comments during research and development preserved. + +@Serializable +data class OverridableColourScheme( + val primary: Int? = null, + val onPrimary: Int? = null, + val primaryContainer: Int? = null, + val onPrimaryContainer: Int? = null, + val inversePrimary: Int? = null, + val secondary: Int? = null, + val onSecondary: Int? = null, + val secondaryContainer: Int? = null, + val onSecondaryContainer: Int? = null, + val tertiary: Int? = null, + val onTertiary: Int? = null, + val tertiaryContainer: Int? = null, + val onTertiaryContainer: Int? = null, + val background: Int? = null, + val onBackground: Int? = null, + val surface: Int? = null, + val onSurface: Int? = null, + val surfaceVariant: Int? = null, + val onSurfaceVariant: Int? = null, + val surfaceTint: Int? = null, + val inverseSurface: Int? = null, + val inverseOnSurface: Int? = null, + val error: Int? = null, + val onError: Int? = null, + val errorContainer: Int? = null, + val onErrorContainer: Int? = null, + val outline: Int? = null, + val outlineVariant: Int? = null, + val scrim: Int? = null +) { + fun applyTo(colorScheme: ColorScheme): ColorScheme { + var newScheme = colorScheme.copy() + + // This is SLOW. It is also STUPID. But using reflection breaks the Compose compiler. + // Another piece of trash from Google. This company should go bankrupt already, what a + // joke. + if (primary != null) newScheme = newScheme.copy(primary = Color(primary)) + if (onPrimary != null) newScheme = newScheme.copy(onPrimary = Color(onPrimary)) + if (primaryContainer != null) newScheme = + newScheme.copy(primaryContainer = Color(primaryContainer)) + if (onPrimaryContainer != null) newScheme = + newScheme.copy(onPrimaryContainer = Color(onPrimaryContainer)) + if (inversePrimary != null) newScheme = + newScheme.copy(inversePrimary = Color(inversePrimary)) + if (secondary != null) newScheme = newScheme.copy(secondary = Color(secondary)) + if (onSecondary != null) newScheme = newScheme.copy(onSecondary = Color(onSecondary)) + if (secondaryContainer != null) newScheme = + newScheme.copy(secondaryContainer = Color(secondaryContainer)) + if (onSecondaryContainer != null) newScheme = + newScheme.copy(onSecondaryContainer = Color(onSecondaryContainer)) + if (tertiary != null) newScheme = newScheme.copy(tertiary = Color(tertiary)) + if (onTertiary != null) newScheme = newScheme.copy(onTertiary = Color(onTertiary)) + if (tertiaryContainer != null) newScheme = + newScheme.copy(tertiaryContainer = Color(tertiaryContainer)) + if (onTertiaryContainer != null) newScheme = + newScheme.copy(onTertiaryContainer = Color(onTertiaryContainer)) + if (background != null) newScheme = newScheme.copy(background = Color(background)) + if (onBackground != null) newScheme = newScheme.copy(onBackground = Color(onBackground)) + if (surface != null) newScheme = newScheme.copy(surface = Color(surface)) + if (onSurface != null) newScheme = newScheme.copy(onSurface = Color(onSurface)) + if (surfaceVariant != null) newScheme = + newScheme.copy(surfaceVariant = Color(surfaceVariant)) + if (onSurfaceVariant != null) newScheme = + newScheme.copy(onSurfaceVariant = Color(onSurfaceVariant)) + if (surfaceTint != null) newScheme = newScheme.copy(surfaceTint = Color(surfaceTint)) + if (inverseSurface != null) newScheme = + newScheme.copy(inverseSurface = Color(inverseSurface)) + if (inverseOnSurface != null) newScheme = + newScheme.copy(inverseOnSurface = Color(inverseOnSurface)) + if (error != null) newScheme = newScheme.copy(error = Color(error)) + if (onError != null) newScheme = newScheme.copy(onError = Color(onError)) + if (errorContainer != null) newScheme = + newScheme.copy(errorContainer = Color(errorContainer)) + if (onErrorContainer != null) newScheme = + newScheme.copy(onErrorContainer = Color(onErrorContainer)) + if (outline != null) newScheme = newScheme.copy(outline = Color(outline)) + if (outlineVariant != null) newScheme = + newScheme.copy(outlineVariant = Color(outlineVariant)) + if (scrim != null) newScheme = newScheme.copy(scrim = Color(scrim)) + + return newScheme + } + + fun applyFromKeyValueMap(map: Map): OverridableColourScheme { + var newScheme = this + + map.filterKeys { it in fieldNames }.forEach { (key, value) -> + when (key) { + "primary" -> newScheme = newScheme.copy(primary = value) + "onPrimary" -> newScheme = newScheme.copy(onPrimary = value) + "primaryContainer" -> newScheme = newScheme.copy(primaryContainer = value) + "onPrimaryContainer" -> newScheme = + newScheme.copy(onPrimaryContainer = value) + + "inversePrimary" -> newScheme = newScheme.copy(inversePrimary = (value)) + "secondary" -> newScheme = newScheme.copy(secondary = (value)) + "onSecondary" -> newScheme = newScheme.copy(onSecondary = (value)) + "secondaryContainer" -> newScheme = + newScheme.copy(secondaryContainer = (value)) + + "onSecondaryContainer" -> newScheme = + newScheme.copy(onSecondaryContainer = (value)) + + "tertiary" -> newScheme = newScheme.copy(tertiary = (value)) + "onTertiary" -> newScheme = newScheme.copy(onTertiary = (value)) + "tertiaryContainer" -> newScheme = newScheme.copy(tertiaryContainer = (value)) + "onTertiaryContainer" -> newScheme = + newScheme.copy(onTertiaryContainer = (value)) + + "background" -> newScheme = newScheme.copy(background = (value)) + "onBackground" -> newScheme = newScheme.copy(onBackground = (value)) + "surface" -> newScheme = newScheme.copy(surface = (value)) + "onSurface" -> newScheme = newScheme.copy(onSurface = (value)) + "surfaceVariant" -> newScheme = newScheme.copy(surfaceVariant = (value)) + "onSurfaceVariant" -> newScheme = newScheme.copy(onSurfaceVariant = (value)) + "surfaceTint" -> newScheme = newScheme.copy(surfaceTint = (value)) + "inverseSurface" -> newScheme = newScheme.copy(inverseSurface = (value)) + "inverseOnSurface" -> newScheme = newScheme.copy(inverseOnSurface = (value)) + "error" -> newScheme = newScheme.copy(error = (value)) + "onError" -> newScheme = newScheme.copy(onError = (value)) + "errorContainer" -> newScheme = newScheme.copy(errorContainer = (value)) + "onErrorContainer" -> newScheme = newScheme.copy(onErrorContainer = (value)) + "outline" -> newScheme = newScheme.copy(outline = (value)) + "outlineVariant" -> newScheme = newScheme.copy(outlineVariant = (value)) + "scrim" -> newScheme = newScheme.copy(scrim = (value)) + } + } + + return newScheme + } + + fun getFieldByName(name: String): Int? { + return when (name) { + "primary" -> primary + "onPrimary" -> onPrimary + "primaryContainer" -> primaryContainer + "onPrimaryContainer" -> onPrimaryContainer + "inversePrimary" -> inversePrimary + "secondary" -> secondary + "onSecondary" -> onSecondary + "secondaryContainer" -> secondaryContainer + "onSecondaryContainer" -> onSecondaryContainer + "tertiary" -> tertiary + "onTertiary" -> onTertiary + "tertiaryContainer" -> tertiaryContainer + "onTertiaryContainer" -> onTertiaryContainer + "background" -> background + "onBackground" -> onBackground + "surface" -> surface + "onSurface" -> onSurface + "surfaceVariant" -> surfaceVariant + "onSurfaceVariant" -> onSurfaceVariant + "surfaceTint" -> surfaceTint + "inverseSurface" -> inverseSurface + "inverseOnSurface" -> inverseOnSurface + "error" -> error + "onError" -> onError + "errorContainer" -> errorContainer + "onErrorContainer" -> onErrorContainer + "outline" -> outline + "outlineVariant" -> outlineVariant + "scrim" -> scrim + else -> null + } + } + + companion object { + // I am genuinely going to go to Google's office and hand them this code and tell them + // to fix their garbage Gradle plugin + val fieldNames = listOf( + "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" + ) + + // See above comment HOLY SHIT i am genuinely going insane + val fieldNameToResource = mapOf( + "primary" to R.string.settings_appearance_colour_overrides_primary, + "onPrimary" to R.string.settings_appearance_colour_overrides_on_primary, + "primaryContainer" to R.string.settings_appearance_colour_overrides_primary_container, + "onPrimaryContainer" to R.string.settings_appearance_colour_overrides_on_primary_container, + "inversePrimary" to R.string.settings_appearance_colour_overrides_inverse_primary, + "secondary" to R.string.settings_appearance_colour_overrides_secondary, + "onSecondary" to R.string.settings_appearance_colour_overrides_on_secondary, + "secondaryContainer" to R.string.settings_appearance_colour_overrides_secondary_container, + "onSecondaryContainer" to R.string.settings_appearance_colour_overrides_on_secondary_container, + "tertiary" to R.string.settings_appearance_colour_overrides_tertiary, + "onTertiary" to R.string.settings_appearance_colour_overrides_on_tertiary, + "tertiaryContainer" to R.string.settings_appearance_colour_overrides_tertiary_container, + "onTertiaryContainer" to R.string.settings_appearance_colour_overrides_on_tertiary_container, + "background" to R.string.settings_appearance_colour_overrides_background, + "onBackground" to R.string.settings_appearance_colour_overrides_on_background, + "surface" to R.string.settings_appearance_colour_overrides_surface, + "onSurface" to R.string.settings_appearance_colour_overrides_on_surface, + "surfaceVariant" to R.string.settings_appearance_colour_overrides_surface_variant, + "onSurfaceVariant" to R.string.settings_appearance_colour_overrides_on_surface_variant, + "surfaceTint" to R.string.settings_appearance_colour_overrides_surface_tint, + "inverseSurface" to R.string.settings_appearance_colour_overrides_inverse_surface, + "inverseOnSurface" to R.string.settings_appearance_colour_overrides_inverse_on_surface, + "error" to R.string.settings_appearance_colour_overrides_error, + "onError" to R.string.settings_appearance_colour_overrides_on_error, + "errorContainer" to R.string.settings_appearance_colour_overrides_error_container, + "onErrorContainer" to R.string.settings_appearance_colour_overrides_on_error_container, + "outline" to R.string.settings_appearance_colour_overrides_outline, + "outlineVariant" to R.string.settings_appearance_colour_overrides_outline_variant, + "scrim" to R.string.settings_appearance_colour_overrides_scrim + ) + } +} + +fun ColorScheme.getFieldByName(name: String): Int? { + return when (name) { + "primary" -> primary.toArgb() + "onPrimary" -> onPrimary.toArgb() + "primaryContainer" -> primaryContainer.toArgb() + "onPrimaryContainer" -> onPrimaryContainer.toArgb() + "inversePrimary" -> inversePrimary.toArgb() + "secondary" -> secondary.toArgb() + "onSecondary" -> onSecondary.toArgb() + "secondaryContainer" -> secondaryContainer.toArgb() + "onSecondaryContainer" -> onSecondaryContainer.toArgb() + "tertiary" -> tertiary.toArgb() + "onTertiary" -> onTertiary.toArgb() + "tertiaryContainer" -> tertiaryContainer.toArgb() + "onTertiaryContainer" -> onTertiaryContainer.toArgb() + "background" -> background.toArgb() + "onBackground" -> onBackground.toArgb() + "surface" -> surface.toArgb() + "onSurface" -> onSurface.toArgb() + "surfaceVariant" -> surfaceVariant.toArgb() + "onSurfaceVariant" -> onSurfaceVariant.toArgb() + "surfaceTint" -> surfaceTint.toArgb() + "inverseSurface" -> inverseSurface.toArgb() + "inverseOnSurface" -> inverseOnSurface.toArgb() + "error" -> error.toArgb() + "onError" -> onError.toArgb() + "errorContainer" -> errorContainer.toArgb() + "onErrorContainer" -> onErrorContainer.toArgb() + "outline" -> outline.toArgb() + "outlineVariant" -> outlineVariant.toArgb() + "scrim" -> scrim.toArgb() + else -> null + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/ui/theme/Theme.kt b/app/src/main/java/chat/revolt/ui/theme/Theme.kt index a6177aaf..0ec510f0 100644 --- a/app/src/main/java/chat/revolt/ui/theme/Theme.kt +++ b/app/src/main/java/chat/revolt/ui/theme/Theme.kt @@ -19,8 +19,6 @@ 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), @@ -68,7 +66,10 @@ enum class Theme { } @Composable -fun getColorScheme(requestedTheme: Theme, colourOverrides: Map? = null): ColorScheme { +fun getColorScheme( + requestedTheme: Theme, + colourOverrides: OverridableColourScheme? = null +): ColorScheme { val context = LocalContext.current val systemInDarkTheme = isSystemInDarkTheme() @@ -112,24 +113,15 @@ fun getColorScheme(requestedTheme: Theme, colourOverrides: Map? = n } } - colorScheme::class.memberProperties.forEach { - if (it is KMutableProperty<*>) { - val name = it.name - val value = colourOverrides?.get(name) - if (value != null) { - it.setter.call(colorScheme, Color(value)) - } - } - } - - return colorScheme + if (colourOverrides == null) return colorScheme + return colourOverrides.applyTo(colorScheme) } @SuppressLint("NewApi") @Composable fun RevoltTheme( requestedTheme: Theme, - colourOverrides: Map?, + colourOverrides: OverridableColourScheme? = null, content: @Composable () -> Unit ) { val colorScheme = getColorScheme(requestedTheme, colourOverrides)