Add color scheme import/export

This commit is contained in:
MM20 2023-08-26 15:11:10 +02:00
parent d8e108cb70
commit 6c040bccce
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
5 changed files with 140 additions and 3 deletions

View File

@ -1,5 +1,7 @@
package de.mm20.launcher2.ui.settings.colorscheme 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box 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.Icons
import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete 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.Edit
import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RadioButtonChecked import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -33,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -51,13 +56,25 @@ import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
fun ThemesSettingsScreen() { fun ThemesSettingsScreen() {
val viewModel: ThemesSettingsScreenVM = viewModel() val viewModel: ThemesSettingsScreenVM = viewModel()
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current
val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle(null) val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle(null)
val themes by viewModel.themes.collectAsStateWithLifecycle(emptyList()) val themes by viewModel.themes.collectAsStateWithLifecycle(emptyList())
var deleteTheme by remember { mutableStateOf<Theme?>(null) } 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 { item {
PreferenceCategory { PreferenceCategory {
for (theme in themes) { for (theme in themes) {
@ -102,6 +119,16 @@ fun ThemesSettingsScreen() {
} }
) )
if (!theme.builtIn) { 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( DropdownMenuItem(
leadingIcon = { leadingIcon = {
Icon(Icons.Rounded.Delete, null) Icon(Icons.Rounded.Delete, null)
@ -127,7 +154,14 @@ fun ThemesSettingsScreen() {
if (deleteTheme != null) { if (deleteTheme != null) {
AlertDialog( AlertDialog(
onDismissRequest = { deleteTheme = null }, 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 = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {

View File

@ -1,20 +1,30 @@
package de.mm20.launcher2.ui.settings.colorscheme package de.mm20.launcher2.ui.settings.colorscheme
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import de.mm20.launcher2.ktx.toBytes import de.mm20.launcher2.ktx.toBytes
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.themes.DefaultThemeId import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.Theme import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.ThemeRepository 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.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File
import java.util.UUID import java.util.UUID
class ThemesSettingsScreenVM : ViewModel(), KoinComponent { class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
@ -57,4 +67,33 @@ class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
fun delete(theme: Theme) { fun delete(theme: Theme) {
themeRepository.deleteTheme(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()))
}
}
}
} }

View File

@ -1,6 +1,7 @@
plugins { plugins {
id("com.android.library") id("com.android.library")
id("kotlin-android") id("kotlin-android")
kotlin("plugin.serialization")
} }
android { android {
@ -36,6 +37,7 @@ android {
dependencies { dependencies {
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)

View File

@ -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)
}

View File

@ -2,6 +2,8 @@ package de.mm20.launcher2.themes
import de.mm20.launcher2.database.entities.ThemeEntity import de.mm20.launcher2.database.entities.ThemeEntity
import hct.Hct import hct.Hct
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.UUID import java.util.UUID
enum class CorePaletteColor { enum class CorePaletteColor {
@ -70,6 +72,7 @@ value class StaticColor(val color: Int) : Color {
} }
} }
@Serializable
data class CorePalette<out T : Int?>( data class CorePalette<out T : Int?>(
val primary: T, val primary: T,
val secondary: T, val secondary: T,
@ -84,6 +87,7 @@ val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
typealias FullCorePalette = CorePalette<Int> typealias FullCorePalette = CorePalette<Int>
typealias PartialCorePalette = CorePalette<Int?> typealias PartialCorePalette = CorePalette<Int?>
@Serializable
data class ColorScheme<out T : Color?>( data class ColorScheme<out T : Color?>(
val primary: T, val primary: T,
val onPrimary: T, val onPrimary: T,
@ -127,8 +131,9 @@ data class ColorScheme<out T : Color?>(
typealias FullColorScheme = ColorScheme<Color> typealias FullColorScheme = ColorScheme<Color>
typealias PartialColorScheme = ColorScheme<Color?> typealias PartialColorScheme = ColorScheme<Color?>
@Serializable
data class Theme( data class Theme(
val id: UUID, @Transient val id: UUID = UUID.randomUUID(),
val builtIn: Boolean = false, val builtIn: Boolean = false,
val name: String, val name: String,
val corePalette: PartialCorePalette = EmptyCorePalette, val corePalette: PartialCorePalette = EmptyCorePalette,