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"
|
||||
|
||||
// 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"
|
||||
|
||||
// Compose BOM
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import chat.revolt.api.internals.Members
|
|||
import chat.revolt.api.realtime.DisconnectionState
|
||||
import chat.revolt.api.realtime.RealtimeSocket
|
||||
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.Message
|
||||
import chat.revolt.api.schemas.Server
|
||||
|
|
@ -38,7 +37,9 @@ import kotlinx.coroutines.runBlocking
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.json.Json
|
||||
import chat.revolt.api.schemas.Channel as ChannelSchema
|
||||
|
||||
const val REVOLT_BASE = "https://api.revolt.chat"
|
||||
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
||||
|
|
@ -61,6 +62,11 @@ val RevoltJson = Json {
|
|||
explicitNulls = false
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val RevoltCbor = Cbor {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
val RevoltHttp = HttpClient(OkHttp) {
|
||||
install(DefaultRequest)
|
||||
install(ContentNegotiation) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package chat.revolt.screens.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
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.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.RevoltCbor
|
||||
import chat.revolt.api.settings.GlobalState
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
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.HsvColorPicker
|
||||
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.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.full.memberProperties
|
||||
|
||||
class AppearanceSettingsScreenViewModel : ViewModel() {
|
||||
@HiltViewModel
|
||||
@Suppress("StaticFieldLeak")
|
||||
class AppearanceSettingsScreenViewModel @Inject constructor(
|
||||
@ApplicationContext val context: Context
|
||||
) : ViewModel() {
|
||||
var showColourOverrides by mutableStateOf(false)
|
||||
var selectedOverrideName by mutableStateOf<String?>(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)
|
||||
@Composable
|
||||
fun AppearanceSettingsScreen(
|
||||
navController: NavController,
|
||||
viewModel: AppearanceSettingsScreenViewModel = viewModel()
|
||||
viewModel: AppearanceSettingsScreenViewModel = hiltViewModel()
|
||||
) {
|
||||
val colourOverridesOpenerArrowRotation by animateFloatAsState(
|
||||
if (viewModel.showColourOverrides) {
|
||||
|
|
@ -131,6 +207,21 @@ fun AppearanceSettingsScreen(
|
|||
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 scope = rememberCoroutineScope()
|
||||
|
||||
|
|
@ -314,6 +405,53 @@ fun AppearanceSettingsScreen(
|
|||
|
||||
AnimatedVisibility(viewModel.showColourOverrides) {
|
||||
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 ->
|
||||
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_export">Export</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_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