feat: import and export theme override (rato) files
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
35c8976e30
commit
3ef9ae2fe9
|
|
@ -171,7 +171,8 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.10"
|
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.6.0"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.6.0"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
|
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
|
||||||
|
|
||||||
// Compose BOM
|
// Compose BOM
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import chat.revolt.api.internals.Members
|
||||||
import chat.revolt.api.realtime.DisconnectionState
|
import chat.revolt.api.realtime.DisconnectionState
|
||||||
import chat.revolt.api.realtime.RealtimeSocket
|
import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.routes.user.fetchSelf
|
import chat.revolt.api.routes.user.fetchSelf
|
||||||
import chat.revolt.api.schemas.Channel as ChannelSchema
|
|
||||||
import chat.revolt.api.schemas.Emoji
|
import chat.revolt.api.schemas.Emoji
|
||||||
import chat.revolt.api.schemas.Message
|
import chat.revolt.api.schemas.Message
|
||||||
import chat.revolt.api.schemas.Server
|
import chat.revolt.api.schemas.Server
|
||||||
|
|
@ -38,7 +37,9 @@ import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.cbor.Cbor
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import chat.revolt.api.schemas.Channel as ChannelSchema
|
||||||
|
|
||||||
const val REVOLT_BASE = "https://api.revolt.chat"
|
const val REVOLT_BASE = "https://api.revolt.chat"
|
||||||
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
||||||
|
|
@ -61,6 +62,11 @@ val RevoltJson = Json {
|
||||||
explicitNulls = false
|
explicitNulls = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
val RevoltCbor = Cbor {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
val RevoltHttp = HttpClient(OkHttp) {
|
val RevoltHttp = HttpClient(OkHttp) {
|
||||||
install(DefaultRequest)
|
install(DefaultRequest)
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package chat.revolt.screens.settings
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -51,14 +55,16 @@ 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.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.RevoltCbor
|
||||||
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
|
||||||
|
|
@ -71,11 +77,22 @@ import com.github.skydoves.colorpicker.compose.BrightnessSlider
|
||||||
import com.github.skydoves.colorpicker.compose.ColorEnvelope
|
import com.github.skydoves.colorpicker.compose.ColorEnvelope
|
||||||
import com.github.skydoves.colorpicker.compose.HsvColorPicker
|
import com.github.skydoves.colorpicker.compose.HsvColorPicker
|
||||||
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
|
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
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.KVisibility
|
||||||
import kotlin.reflect.full.memberProperties
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
class AppearanceSettingsScreenViewModel : ViewModel() {
|
@HiltViewModel
|
||||||
|
@Suppress("StaticFieldLeak")
|
||||||
|
class AppearanceSettingsScreenViewModel @Inject constructor(
|
||||||
|
@ApplicationContext val context: Context
|
||||||
|
) : ViewModel() {
|
||||||
var showColourOverrides by mutableStateOf(false)
|
var showColourOverrides by mutableStateOf(false)
|
||||||
var selectedOverrideName by mutableStateOf<String?>(null)
|
var selectedOverrideName by mutableStateOf<String?>(null)
|
||||||
var selectedOverrideInitialValue by mutableStateOf<Int?>(null)
|
var selectedOverrideInitialValue by mutableStateOf<Int?>(null)
|
||||||
|
|
@ -116,13 +133,72 @@ class AppearanceSettingsScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validOverrideKey(key: String): Boolean {
|
||||||
|
return ColorScheme::class.memberProperties.any { it.name == key }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyBulkOverrides(overrides: Map<String, Int>) {
|
||||||
|
val existingOverrides = SyncedSettings.android.colourOverrides ?: mapOf()
|
||||||
|
val newOverrides = existingOverrides.toMutableMap()
|
||||||
|
|
||||||
|
newOverrides.putAll(overrides.filterKeys { validOverrideKey(it) })
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
SyncedSettings.updateAndroid(
|
||||||
|
SyncedSettings.android.copy(
|
||||||
|
colourOverrides = newOverrides
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
fun processImportedOverrides(uri: Uri) {
|
||||||
|
val mFile = File(context.cacheDir, uri.lastPathSegment ?: "temp")
|
||||||
|
|
||||||
|
mFile.outputStream().use { outputStream ->
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
RevoltCbor.decodeFromByteArray(
|
||||||
|
MapSerializer(String.serializer(), Int.serializer()),
|
||||||
|
mFile.readBytes()
|
||||||
|
).let {
|
||||||
|
applyBulkOverrides(it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.settings_appearance_colour_overrides_import_error),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
mFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
fun saveOverridesToFile(uri: Uri) {
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
outputStream.write(
|
||||||
|
RevoltCbor.encodeToByteArray(
|
||||||
|
MapSerializer(String.serializer(), Int.serializer()),
|
||||||
|
SyncedSettings.android.colourOverrides ?: mapOf()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppearanceSettingsScreen(
|
fun AppearanceSettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: AppearanceSettingsScreenViewModel = viewModel()
|
viewModel: AppearanceSettingsScreenViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val colourOverridesOpenerArrowRotation by animateFloatAsState(
|
val colourOverridesOpenerArrowRotation by animateFloatAsState(
|
||||||
if (viewModel.showColourOverrides) {
|
if (viewModel.showColourOverrides) {
|
||||||
|
|
@ -131,6 +207,21 @@ fun AppearanceSettingsScreen(
|
||||||
label = "colourOverridesOpenerArrowRotation"
|
label = "colourOverridesOpenerArrowRotation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val filePicker = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
viewModel.processImportedOverrides(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val fileSaver = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.CreateDocument("application/x-revolt-android-theme-overrides"),
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
viewModel.saveOverridesToFile(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
@ -314,6 +405,53 @@ fun AppearanceSettingsScreen(
|
||||||
|
|
||||||
AnimatedVisibility(viewModel.showColourOverrides) {
|
AnimatedVisibility(viewModel.showColourOverrides) {
|
||||||
Column {
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
filePicker.launch(arrayOf("*/*"))
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_folder_24dp),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_appearance_colour_overrides_import)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
fileSaver.launch("${SyncedSettings.android.theme}-colours.rato")
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_content_save_24dp),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_appearance_colour_overrides_export)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
ColorScheme::class.memberProperties.forEach { member ->
|
ColorScheme::class.memberProperties.forEach { member ->
|
||||||
if (member.visibility != KVisibility.PUBLIC) return@forEach
|
if (member.visibility != KVisibility.PUBLIC) return@forEach
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -455,6 +455,7 @@
|
||||||
<string name="settings_appearance_colour_overrides_reset">Reset</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_export">Export</string>
|
||||||
<string name="settings_appearance_colour_overrides_import">Import</string>
|
<string name="settings_appearance_colour_overrides_import">Import</string>
|
||||||
|
<string name="settings_appearance_colour_overrides_import_error">This file is not a valid colour override file.</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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue