Add color scheme import/export
This commit is contained in:
parent
d8e108cb70
commit
6c040bccce
@ -1,5 +1,7 @@
|
||||
package de.mm20.launcher2.ui.settings.colorscheme
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -13,10 +15,12 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Download
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material.icons.rounded.RadioButtonChecked
|
||||
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
|
||||
import androidx.compose.material.icons.rounded.Share
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@ -33,6 +37,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -51,13 +56,25 @@ import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
|
||||
fun ThemesSettingsScreen() {
|
||||
val viewModel: ThemesSettingsScreenVM = viewModel()
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle(null)
|
||||
val themes by viewModel.themes.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
var deleteTheme by remember { mutableStateOf<Theme?>(null) }
|
||||
|
||||
PreferenceScreen(title = stringResource(R.string.preference_screen_colors)) {
|
||||
val importIntentLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
viewModel.importTheme(context, it)
|
||||
}
|
||||
|
||||
PreferenceScreen(
|
||||
title = stringResource(R.string.preference_screen_colors),
|
||||
topBarActions = {
|
||||
IconButton(onClick = { importIntentLauncher.launch(arrayOf("*/*")) }) {
|
||||
Icon(Icons.Rounded.Download, null)
|
||||
}
|
||||
}
|
||||
) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
for (theme in themes) {
|
||||
@ -102,6 +119,16 @@ fun ThemesSettingsScreen() {
|
||||
}
|
||||
)
|
||||
if (!theme.builtIn) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.Share, null)
|
||||
},
|
||||
text = { Text(stringResource(R.string.menu_share)) },
|
||||
onClick = {
|
||||
viewModel.exportTheme(context, theme)
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.Delete, null)
|
||||
@ -127,7 +154,14 @@ fun ThemesSettingsScreen() {
|
||||
if (deleteTheme != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { deleteTheme = null },
|
||||
text = { Text(stringResource(R.string.confirmation_delete_color_scheme, deleteTheme!!.name)) },
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.confirmation_delete_color_scheme,
|
||||
deleteTheme!!.name
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
||||
@ -1,20 +1,30 @@
|
||||
package de.mm20.launcher2.ui.settings.colorscheme
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.protobuf.ByteString
|
||||
import de.mm20.launcher2.ktx.toBytes
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.themes.DefaultThemeId
|
||||
import de.mm20.launcher2.themes.Theme
|
||||
import de.mm20.launcher2.themes.ThemeRepository
|
||||
import de.mm20.launcher2.themes.fromJson
|
||||
import de.mm20.launcher2.themes.toJson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
@ -57,4 +67,33 @@ class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun delete(theme: Theme) {
|
||||
themeRepository.deleteTheme(theme)
|
||||
}
|
||||
|
||||
fun exportTheme(context: Context, theme: Theme) {
|
||||
viewModelScope.launch {
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, "${theme.name}.kvtheme")
|
||||
file.writeText(theme.toJson())
|
||||
file
|
||||
}
|
||||
context.tryStartActivity(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "application/json"
|
||||
putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(
|
||||
context,
|
||||
context.applicationContext.packageName + ".fileprovider",
|
||||
file
|
||||
))
|
||||
}.let { Intent.createChooser(it, null) })
|
||||
}
|
||||
}
|
||||
|
||||
fun importTheme(context: Context, uri: Uri?) {
|
||||
uri ?: return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri)?.use {
|
||||
val theme = Theme.fromJson(it.readBytes().toString(Charsets.UTF_8))
|
||||
themeRepository.createTheme(theme.copy(id = UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -36,6 +37,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
package de.mm20.launcher2.themes
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
|
||||
internal class ColorRefSerializer: KSerializer<ColorRef> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("$", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ColorRef {
|
||||
return Color(decoder.decodeString()) as ColorRef
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ColorRef) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal class StaticColorSerializer: KSerializer<StaticColor> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("#", PrimitiveKind.STRING)
|
||||
override fun deserialize(decoder: Decoder): StaticColor {
|
||||
return Color(decoder.decodeString()) as StaticColor
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: StaticColor) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal val module = SerializersModule {
|
||||
polymorphic(Color::class) {
|
||||
subclass(ColorRef::class, ColorRefSerializer())
|
||||
subclass(StaticColor::class, StaticColorSerializer())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val ThemeJson = Json {
|
||||
serializersModule = module
|
||||
useArrayPolymorphism = true
|
||||
}
|
||||
|
||||
fun Theme.toJson(): String {
|
||||
return ThemeJson.encodeToString(this)
|
||||
}
|
||||
|
||||
fun Theme.Companion.fromJson(json: String): Theme {
|
||||
return ThemeJson.decodeFromString(json)
|
||||
}
|
||||
@ -2,6 +2,8 @@ package de.mm20.launcher2.themes
|
||||
|
||||
import de.mm20.launcher2.database.entities.ThemeEntity
|
||||
import hct.Hct
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.util.UUID
|
||||
|
||||
enum class CorePaletteColor {
|
||||
@ -70,6 +72,7 @@ value class StaticColor(val color: Int) : Color {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CorePalette<out T : Int?>(
|
||||
val primary: T,
|
||||
val secondary: T,
|
||||
@ -84,6 +87,7 @@ val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
|
||||
typealias FullCorePalette = CorePalette<Int>
|
||||
typealias PartialCorePalette = CorePalette<Int?>
|
||||
|
||||
@Serializable
|
||||
data class ColorScheme<out T : Color?>(
|
||||
val primary: T,
|
||||
val onPrimary: T,
|
||||
@ -127,8 +131,9 @@ data class ColorScheme<out T : Color?>(
|
||||
typealias FullColorScheme = ColorScheme<Color>
|
||||
typealias PartialColorScheme = ColorScheme<Color?>
|
||||
|
||||
@Serializable
|
||||
data class Theme(
|
||||
val id: UUID,
|
||||
@Transient val id: UUID = UUID.randomUUID(),
|
||||
val builtIn: Boolean = false,
|
||||
val name: String,
|
||||
val corePalette: PartialCorePalette = EmptyCorePalette,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user