From 6c040bccceb0ddd0cf56000971da4e5f29f53153 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 26 Aug 2023 15:11:10 +0200 Subject: [PATCH] Add color scheme import/export --- .../colorscheme/ThemesSettingsScreen.kt | 38 ++++++++++++- .../colorscheme/ThemesSettingsScreenVM.kt | 39 +++++++++++++ data/themes/build.gradle.kts | 2 + .../de/mm20/launcher2/themes/Serialization.kt | 57 +++++++++++++++++++ .../java/de/mm20/launcher2/themes/Theme.kt | 7 ++- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt index 2a48a88b..080c6b35 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt @@ -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(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 = { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt index 3ed6dafe..83f639a2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt @@ -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())) + } + } + } } \ No newline at end of file diff --git a/data/themes/build.gradle.kts b/data/themes/build.gradle.kts index bf1f844d..9955d6cc 100644 --- a/data/themes/build.gradle.kts +++ b/data/themes/build.gradle.kts @@ -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) diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt new file mode 100644 index 00000000..c3d8bbfb --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt @@ -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 { + 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 { + 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) +} \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt index 4c8ad761..00ab1141 100644 --- a/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt @@ -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( val primary: T, val secondary: T, @@ -84,6 +87,7 @@ val EmptyCorePalette = CorePalette(null, null, null, null, null, null) typealias FullCorePalette = CorePalette typealias PartialCorePalette = CorePalette +@Serializable data class ColorScheme( val primary: T, val onPrimary: T, @@ -127,8 +131,9 @@ data class ColorScheme( typealias FullColorScheme = ColorScheme typealias PartialColorScheme = ColorScheme +@Serializable data class Theme( - val id: UUID, + @Transient val id: UUID = UUID.randomUUID(), val builtIn: Boolean = false, val name: String, val corePalette: PartialCorePalette = EmptyCorePalette,