Add import for new color scheme format
This commit is contained in:
parent
f21feba000
commit
889aa37915
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -46,7 +45,6 @@ import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.preferences.SearchBarStyle
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
|
||||
|
||||
@Composable
|
||||
@ -61,6 +59,7 @@ fun SearchBar(
|
||||
onUnfocus: () -> Unit = {},
|
||||
reverse: Boolean = false,
|
||||
darkColors: Boolean = false,
|
||||
readOnly: Boolean = false,
|
||||
menu: @Composable RowScope.() -> Unit = {},
|
||||
actions: @Composable ColumnScope.() -> Unit = {},
|
||||
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
|
||||
@ -190,7 +189,8 @@ fun SearchBar(
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = onKeyboardActionGo,
|
||||
)
|
||||
),
|
||||
readOnly = readOnly,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
|
||||
@ -6,6 +6,7 @@ import de.mm20.launcher2.ui.base.BaseActivity
|
||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
||||
import de.mm20.launcher2.ui.common.ImportThemeSheet
|
||||
import de.mm20.launcher2.ui.overlays.OverlayHost
|
||||
import de.mm20.launcher2.ui.settings.appearance.ImportThemeSettingsScreen
|
||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||
|
||||
class ImportThemeActivity : BaseActivity() {
|
||||
@ -19,10 +20,7 @@ class ImportThemeActivity : BaseActivity() {
|
||||
LauncherTheme {
|
||||
ProvideSettings {
|
||||
OverlayHost {
|
||||
ImportThemeSheet(
|
||||
onDismiss = { finish() },
|
||||
uri = uri,
|
||||
)
|
||||
ImportThemeSettingsScreen(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package de.mm20.launcher2.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
@ -22,11 +23,13 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.toRoute
|
||||
import de.mm20.launcher2.licenses.AppLicense
|
||||
import de.mm20.launcher2.licenses.OpenSourceLicenses
|
||||
import de.mm20.launcher2.ui.base.BaseActivity
|
||||
@ -38,6 +41,8 @@ import de.mm20.launcher2.ui.overlays.OverlayHost
|
||||
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.appearance.ExportThemeSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.appearance.ImportThemeSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.appearance.ImportThemeSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.breezyweather.BreezyWeatherSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
|
||||
@ -163,6 +168,10 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/appearance/export") {
|
||||
ExportThemeSettingsScreen()
|
||||
}
|
||||
composable<ImportThemeSettingsRoute> {
|
||||
val route: ImportThemeSettingsRoute = it.toRoute() ?: return@composable
|
||||
ImportThemeSettingsScreen(route.fromUri.toUri())
|
||||
}
|
||||
composable("settings/homescreen") {
|
||||
HomescreenSettingsScreen()
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package de.mm20.launcher2.ui.settings.appearance
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowCircleDown
|
||||
import androidx.compose.material.icons.rounded.ArrowCircleUp
|
||||
@ -35,6 +37,14 @@ fun AppearanceSettingsScreen() {
|
||||
val colorThemeName by viewModel.colorThemeName.collectAsStateWithLifecycle(null)
|
||||
val shapeThemeName by viewModel.shapeThemeName.collectAsStateWithLifecycle(null)
|
||||
val compatModeColors by viewModel.compatModeColors.collectAsState()
|
||||
|
||||
val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
if (it == null) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
navController?.navigate(ImportThemeSettingsRoute(it.toString()))
|
||||
}
|
||||
|
||||
PreferenceScreen(title = stringResource(id = R.string.preference_screen_appearance)) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
@ -105,11 +115,14 @@ fun AppearanceSettingsScreen() {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
Preference(
|
||||
title = "Import",
|
||||
title = stringResource(R.string.theme_import_title),
|
||||
icon = Icons.Rounded.ArrowCircleDown,
|
||||
onClick = {
|
||||
importLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = "Export",
|
||||
title = stringResource(R.string.theme_export_title),
|
||||
icon = Icons.Rounded.ArrowCircleUp,
|
||||
onClick = {
|
||||
navController?.navigate("settings/appearance/export")
|
||||
|
||||
@ -0,0 +1,350 @@
|
||||
package de.mm20.launcher2.ui.settings.appearance
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ChangeCircle
|
||||
import androidx.compose.material.icons.rounded.CropSquare
|
||||
import androidx.compose.material.icons.rounded.DarkMode
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.ErrorOutline
|
||||
import androidx.compose.material.icons.rounded.LightMode
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.material.icons.rounded.PublishedWithChanges
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.Upgrade
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.draw.innerShadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.shadow.InnerShadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.preferences.SearchBarStyle
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.component.SearchBar
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
||||
import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
|
||||
import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
|
||||
import de.mm20.launcher2.ui.theme.shapes.shapesOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ImportThemeSettingsRoute(val fromUri: String)
|
||||
|
||||
@Composable
|
||||
fun ImportThemeSettingsScreen(
|
||||
fromUri: Uri,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = LocalActivity.current
|
||||
val viewModel: ImportThemeSettingsScreenVM = viewModel()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val themeBundle = viewModel.themeBundle
|
||||
|
||||
LaunchedEffect(fromUri) {
|
||||
viewModel.init(context, fromUri)
|
||||
}
|
||||
|
||||
PreferenceScreen(title = stringResource(R.string.theme_import_title)) {
|
||||
|
||||
if (viewModel.error) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
Banner(
|
||||
text = stringResource(R.string.import_theme_error),
|
||||
icon = Icons.Rounded.ErrorOutline,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (themeBundle != null) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
Preference(
|
||||
title = themeBundle.name,
|
||||
summary = themeBundle.author?.takeIf { it.isNotBlank() })
|
||||
}
|
||||
}
|
||||
item {
|
||||
val isDarkMode = LocalDarkTheme.current
|
||||
var darkModePreview by remember { mutableStateOf(isDarkMode) }
|
||||
MaterialTheme(
|
||||
colorScheme = themeBundle.colors?.let {
|
||||
if (darkModePreview) darkColorSchemeOf(it) else lightColorSchemeOf(it)
|
||||
} ?: MaterialTheme.colorScheme,
|
||||
shapes = themeBundle.shapes?.let { shapesOf(it) } ?: MaterialTheme.shapes,
|
||||
) {
|
||||
ThemePreview(
|
||||
darkMode = darkModePreview,
|
||||
onDarkModeChanged = { darkModePreview = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceCategory {
|
||||
if (themeBundle.colors != null) {
|
||||
Preference(
|
||||
icon = Icons.Rounded.Palette,
|
||||
title = stringResource(R.string.preference_screen_colors),
|
||||
summary = themeBundle.colors?.name,
|
||||
controls = if (viewModel.colorsExists) {
|
||||
{
|
||||
Icon(Icons.Rounded.ChangeCircle, null)
|
||||
}
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
if (themeBundle.shapes != null) {
|
||||
Preference(
|
||||
icon = Icons.Rounded.CropSquare,
|
||||
title = stringResource(R.string.preference_screen_shapes),
|
||||
summary = themeBundle.shapes?.name,
|
||||
controls = if (viewModel.shapesExists) {
|
||||
{
|
||||
Icon(Icons.Rounded.ChangeCircle, null)
|
||||
}
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
if (viewModel.colorsExists || viewModel.shapesExists) {
|
||||
Banner(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
MaterialTheme.shapes.extraSmall
|
||||
)
|
||||
.padding(16.dp),
|
||||
icon = Icons.Rounded.ChangeCircle,
|
||||
text = stringResource(R.string.import_theme_exists)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceCategory {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.import_theme_apply),
|
||||
value = viewModel.applyTheme,
|
||||
onValueChanged = { viewModel.applyTheme = it },
|
||||
)
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
MaterialTheme.shapes.extraSmall
|
||||
)
|
||||
.padding(16.dp),
|
||||
enabled = !viewModel.loading,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.import()?.join()
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.action_import))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemePreview(
|
||||
darkMode: Boolean,
|
||||
onDarkModeChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainer,
|
||||
MaterialTheme.shapes.medium
|
||||
)
|
||||
.innerShadow(
|
||||
MaterialTheme.shapes.medium,
|
||||
InnerShadow(8.dp, color = Color(0f, 0f, 0f, 0.2f))
|
||||
)
|
||||
) {
|
||||
SearchBar(
|
||||
style = SearchBarStyle.Solid,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, start = 12.dp, end = 12.dp),
|
||||
level = SearchBarLevel.Active,
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
menu = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(Icons.Rounded.MoreVert, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, start = 12.dp, end = 12.dp)
|
||||
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.medium)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.secondaryContainer,
|
||||
MaterialTheme.shapes.small
|
||||
),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
"Title",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
"Subtitle",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Text(
|
||||
"Body",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
label = { Text("Chip") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Rounded.Star, null,
|
||||
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
},
|
||||
onClick = { },
|
||||
)
|
||||
FilterChip(
|
||||
selected = false,
|
||||
label = { Text("Chip") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Rounded.Star, null,
|
||||
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
},
|
||||
onClick = { },
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
FilledTonalIconButton(onClick = {}) {
|
||||
Icon(Icons.Rounded.Edit, null)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
) {
|
||||
Button(onClick = {}) { Text("Button") }
|
||||
OutlinedButton(onClick = {}) { Text("Button") }
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow {
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
|
||||
selected = !darkMode,
|
||||
onClick = { onDarkModeChanged(false) }
|
||||
) {
|
||||
Icon(Icons.Rounded.LightMode, null)
|
||||
}
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
|
||||
selected = darkMode,
|
||||
onClick = { onDarkModeChanged(true) }
|
||||
) {
|
||||
Icon(Icons.Rounded.DarkMode, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ThemePreviewPreview() {
|
||||
ThemePreview(
|
||||
darkMode = false,
|
||||
onDarkModeChanged = {}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package de.mm20.launcher2.ui.settings.appearance
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.ColorsDescriptor
|
||||
import de.mm20.launcher2.preferences.ShapesDescriptor
|
||||
import de.mm20.launcher2.preferences.ui.UiSettings
|
||||
import de.mm20.launcher2.themes.ThemeBundle
|
||||
import de.mm20.launcher2.themes.ThemeRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.getValue
|
||||
|
||||
class ImportThemeSettingsScreenVM: ViewModel(), KoinComponent {
|
||||
|
||||
private val themeRepository by inject<ThemeRepository>()
|
||||
private val uiSettings by inject<UiSettings>()
|
||||
|
||||
var themeBundle by mutableStateOf<ThemeBundle?>(null)
|
||||
private set
|
||||
|
||||
var colorsExists by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var shapesExists by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var loading by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var error by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var applyTheme by mutableStateOf(true)
|
||||
|
||||
fun init(context: Context, fromUri: Uri) {
|
||||
themeBundle = null
|
||||
error = false
|
||||
applyTheme = true
|
||||
loading = true
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
context.contentResolver.openInputStream(fromUri)?.reader()?.use {
|
||||
val text = it.readText()
|
||||
val theme = ThemeBundle.fromJson(text)
|
||||
if (theme != null) {
|
||||
val colors = theme.colors?.id?.let { themeRepository.getColors(it) }?.first()
|
||||
val shapes = theme.shapes?.id?.let { themeRepository.getShapes(it) }?.first()
|
||||
|
||||
colorsExists = colors != null
|
||||
shapesExists = shapes != null
|
||||
themeBundle = theme
|
||||
loading = false
|
||||
} else {
|
||||
error = true
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
CrashReporter.logException(e)
|
||||
error = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun import(): Job? {
|
||||
val themeBundle = this.themeBundle ?: return null
|
||||
|
||||
val colors = themeBundle.colors
|
||||
val shapes = themeBundle.shapes
|
||||
|
||||
val colorsExist = this.colorsExists
|
||||
val shapesExist = this.shapesExists
|
||||
|
||||
loading = true
|
||||
return viewModelScope.launch {
|
||||
if (colors != null) {
|
||||
if (colorsExist) {
|
||||
themeRepository.updateColors(colors)
|
||||
} else {
|
||||
themeRepository.createColors(colors)
|
||||
}
|
||||
if (applyTheme) {
|
||||
uiSettings.setColors(ColorsDescriptor.Custom(colors.id.toString()))
|
||||
}
|
||||
}
|
||||
if (shapes != null) {
|
||||
if (shapesExist) {
|
||||
themeRepository.updateShapes(shapes)
|
||||
} else {
|
||||
themeRepository.createShapes(shapes)
|
||||
}
|
||||
if (applyTheme) {
|
||||
uiSettings.setShapes(ShapesDescriptor.Custom(shapes.id.toString()))
|
||||
}
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -97,7 +97,8 @@ fun CorePaletteColorPreference(
|
||||
value = currentValue == null,
|
||||
onValueChanged = {
|
||||
currentValue = if (it) null else defaultValue
|
||||
}
|
||||
},
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
currentValue != null,
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import androidx.core.graphics.toColorInt
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
object ColorIntAsHexSerializer : KSerializer<Int> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Int {
|
||||
return decoder.decodeString().toColorInt()
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Int) {
|
||||
encoder.encodeString("#" + value.toUInt().toString(16).padStart(8, '0'))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.util.UUID
|
||||
|
||||
object UUIDSerializer: KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
|
||||
"UUIDSerializer",
|
||||
PrimitiveKind.STRING
|
||||
)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID {
|
||||
val string = decoder.decodeString()
|
||||
return try {
|
||||
UUID.fromString(string)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw SerializationException("Invalid UUID format: $string", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -828,6 +828,7 @@
|
||||
<string name="theme_color_scheme_custom_color">Custom</string>
|
||||
<string name="preference_restore_default">Restore default</string>
|
||||
<string name="import_theme_apply">Apply theme</string>
|
||||
<string name="import_theme_exists">Theme already exists and will be updated</string>
|
||||
<string name="import_theme_error">The selected file could not be read. Please make sure that you selected a valid theme file (*.kvtheme), and that the file is not corrupt.</string>
|
||||
<string name="shortcut_label_unavailable">Unavailable</string>
|
||||
<string name="app_label_locked_profile">Locked</string>
|
||||
@ -1032,6 +1033,7 @@
|
||||
<string name="bad_configuration_title">Congratulations, you\'ve locked yourself out!</string>
|
||||
<string name="bad_configuration_summary">You\'ve discovered a combination of settings that makes both the search and settings inaccessible — effectively locking you out of the launcher.</string>
|
||||
<string name="theme_export_title">Export theme</string>
|
||||
<string name="theme_import_title">Import theme</string>
|
||||
<string name="theme_bundle_name">Name</string>
|
||||
<string name="theme_bundle_author">Author</string>
|
||||
</resources>
|
||||
@ -1,14 +1,16 @@
|
||||
package de.mm20.launcher2.themes
|
||||
|
||||
import de.mm20.launcher2.database.entities.ColorsEntity
|
||||
import de.mm20.launcher2.serialization.ColorIntAsHexSerializer
|
||||
import de.mm20.launcher2.serialization.UUIDSerializer
|
||||
import hct.Hct
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class Colors(
|
||||
@Transient val id: UUID = UUID.randomUUID(),
|
||||
@Serializable(with = UUIDSerializer::class) val id: UUID = UUID.randomUUID(),
|
||||
val builtIn: Boolean = false,
|
||||
val name: String,
|
||||
val corePalette: PartialCorePalette = EmptyCorePalette,
|
||||
@ -229,7 +231,6 @@ enum class CorePaletteColor {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = ColorSerializer::class)
|
||||
sealed interface Color {
|
||||
companion object {
|
||||
fun fromString(string: String?): Color? {
|
||||
@ -267,6 +268,8 @@ value class StaticColor(val color: Int) : Color {
|
||||
}
|
||||
}
|
||||
|
||||
typealias CorePaletteColorValue = @Serializable(with = ColorIntAsHexSerializer::class) Int
|
||||
|
||||
@Serializable
|
||||
data class CorePalette<out T : Int?>(
|
||||
val primary: T,
|
||||
@ -277,50 +280,50 @@ data class CorePalette<out T : Int?>(
|
||||
val error: T,
|
||||
)
|
||||
|
||||
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
|
||||
val EmptyCorePalette = CorePalette<CorePaletteColorValue?>(null, null, null, null, null, null)
|
||||
|
||||
typealias FullCorePalette = CorePalette<Int>
|
||||
typealias PartialCorePalette = CorePalette<Int?>
|
||||
typealias FullCorePalette = CorePalette<CorePaletteColorValue>
|
||||
typealias PartialCorePalette = CorePalette<CorePaletteColorValue?>
|
||||
|
||||
@Serializable
|
||||
data class ColorScheme<out T : Color?>(
|
||||
val primary: T,
|
||||
val onPrimary: T,
|
||||
val primaryContainer: T,
|
||||
val onPrimaryContainer: T,
|
||||
val secondary: T,
|
||||
val onSecondary: T,
|
||||
val secondaryContainer: T,
|
||||
val onSecondaryContainer: T,
|
||||
val tertiary: T,
|
||||
val onTertiary: T,
|
||||
val tertiaryContainer: T,
|
||||
val onTertiaryContainer: T,
|
||||
val error: T,
|
||||
val onError: T,
|
||||
val errorContainer: T,
|
||||
val onErrorContainer: T,
|
||||
val surface: T,
|
||||
val onSurface: T,
|
||||
val onSurfaceVariant: T,
|
||||
val outline: T,
|
||||
val outlineVariant: T,
|
||||
val inverseSurface: T,
|
||||
val inverseOnSurface: T,
|
||||
val inversePrimary: T,
|
||||
val surfaceDim: T,
|
||||
val surfaceBright: T,
|
||||
val surfaceContainerLowest: T,
|
||||
val surfaceContainerLow: T,
|
||||
val surfaceContainer: T,
|
||||
val surfaceContainerHigh: T,
|
||||
val surfaceContainerHighest: T,
|
||||
@Contextual val primary: T,
|
||||
@Contextual val onPrimary: T,
|
||||
@Contextual val primaryContainer: T,
|
||||
@Contextual val onPrimaryContainer: T,
|
||||
@Contextual val secondary: T,
|
||||
@Contextual val onSecondary: T,
|
||||
@Contextual val secondaryContainer: T,
|
||||
@Contextual val onSecondaryContainer: T,
|
||||
@Contextual val tertiary: T,
|
||||
@Contextual val onTertiary: T,
|
||||
@Contextual val tertiaryContainer: T,
|
||||
@Contextual val onTertiaryContainer: T,
|
||||
@Contextual val error: T,
|
||||
@Contextual val onError: T,
|
||||
@Contextual val errorContainer: T,
|
||||
@Contextual val onErrorContainer: T,
|
||||
@Contextual val surface: T,
|
||||
@Contextual val onSurface: T,
|
||||
@Contextual val onSurfaceVariant: T,
|
||||
@Contextual val outline: T,
|
||||
@Contextual val outlineVariant: T,
|
||||
@Contextual val inverseSurface: T,
|
||||
@Contextual val inverseOnSurface: T,
|
||||
@Contextual val inversePrimary: T,
|
||||
@Contextual val surfaceDim: T,
|
||||
@Contextual val surfaceBright: T,
|
||||
@Contextual val surfaceContainerLowest: T,
|
||||
@Contextual val surfaceContainerLow: T,
|
||||
@Contextual val surfaceContainer: T,
|
||||
@Contextual val surfaceContainerHigh: T,
|
||||
@Contextual val surfaceContainerHighest: T,
|
||||
|
||||
val background: T,
|
||||
val onBackground: T,
|
||||
val surfaceTint: T,
|
||||
val scrim: T,
|
||||
val surfaceVariant: T,
|
||||
@Contextual val background: T,
|
||||
@Contextual val onBackground: T,
|
||||
@Contextual val surfaceTint: T,
|
||||
@Contextual val scrim: T,
|
||||
@Contextual val surfaceVariant: T,
|
||||
)
|
||||
|
||||
typealias FullColorScheme = ColorScheme<Color>
|
||||
|
||||
@ -6,8 +6,25 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
|
||||
internal class ColorSerializer: KSerializer<Color> {
|
||||
internal val module = SerializersModule {
|
||||
contextual(Color::class, ColorSerializer)
|
||||
|
||||
}
|
||||
|
||||
val ThemeJson = Json {
|
||||
serializersModule = module
|
||||
encodeDefaults = true
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
isLenient = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
internal object ColorSerializer: KSerializer<Color> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(
|
||||
@ -18,12 +35,12 @@ internal class ColorSerializer: KSerializer<Color> {
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Color {
|
||||
TODO("Not yet implemented")
|
||||
val stringValue = decoder.decodeString()
|
||||
return Color.fromString(stringValue) ?: StaticColor(0xFF000000.toInt())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class ShapeSerializer: KSerializer<Shape> {
|
||||
internal object ShapeSerializer: KSerializer<Shape> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
package de.mm20.launcher2.themes
|
||||
|
||||
import de.mm20.launcher2.database.entities.ShapesEntity
|
||||
import de.mm20.launcher2.serialization.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class Shapes(
|
||||
@Transient val id: UUID = UUID.randomUUID(),
|
||||
@Serializable(with = UUIDSerializer::class) val id: UUID = UUID.randomUUID(),
|
||||
val builtIn: Boolean = false,
|
||||
val name: String,
|
||||
val baseShape: Shape = Shape(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package de.mm20.launcher2.themes
|
||||
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -7,10 +8,13 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ThemeBundle(
|
||||
@ -24,21 +28,23 @@ data class ThemeBundle(
|
||||
val version: Int = 2,
|
||||
) {
|
||||
fun toJson(): String {
|
||||
return Json.Lenient.encodeToString(this)
|
||||
return ThemeJson.encodeToString(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJson(jsonString: String): ThemeBundle? {
|
||||
try {
|
||||
val jsonElement = Json.Lenient.parseToJsonElement(jsonString).jsonObject
|
||||
val jsonElement = ThemeJson.parseToJsonElement(jsonString).jsonObject
|
||||
|
||||
val version = (jsonElement.get("version") as? JsonPrimitive)?.intOrNull
|
||||
val version = (jsonElement["version"] as? JsonPrimitive)?.intOrNull
|
||||
|
||||
if (version != 2) {
|
||||
return fromLegacyJson(jsonElement)
|
||||
}
|
||||
|
||||
return Json.Lenient.decodeFromJsonElement(jsonElement)
|
||||
return ThemeJson.decodeFromJsonElement<ThemeBundle>(jsonElement).also {
|
||||
Log.d("MM20", "$it")
|
||||
}
|
||||
|
||||
} catch (e: SerializationException) {
|
||||
CrashReporter.logException(e)
|
||||
@ -49,9 +55,26 @@ data class ThemeBundle(
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromLegacyJson(jsonElement: JsonElement): ThemeBundle? {
|
||||
private fun fromLegacyJson(jsonElement: JsonObject): ThemeBundle? {
|
||||
try {
|
||||
val colorScheme: Colors = LegacyThemeJson.decodeFromJsonElement(jsonElement)
|
||||
val name = (jsonElement["name"] as? JsonPrimitive)?.contentOrNull ?: return null
|
||||
val corePalette = (jsonElement["corePalette"] as? JsonObject)?.let {
|
||||
LegacyThemeJson.decodeFromJsonElement<CorePalette<Int?>>(it)
|
||||
}
|
||||
val lightColorScheme = (jsonElement["lightColorScheme"] as? JsonObject)?.let {
|
||||
LegacyThemeJson.decodeFromJsonElement<ColorScheme<Color?>>(it)
|
||||
}
|
||||
val darkColorScheme = (jsonElement["darkColorScheme"] as? JsonObject)?.let {
|
||||
LegacyThemeJson.decodeFromJsonElement<ColorScheme<Color?>>(it)
|
||||
}
|
||||
|
||||
val colorScheme = Colors(
|
||||
id = UUID.randomUUID(),
|
||||
name = name,
|
||||
corePalette = corePalette ?: EmptyCorePalette,
|
||||
lightColorScheme = lightColorScheme ?: DefaultLightColorScheme,
|
||||
darkColorScheme = darkColorScheme ?: DefaultDarkColorScheme,
|
||||
)
|
||||
return ThemeBundle(
|
||||
name = colorScheme.name,
|
||||
author = "",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user