From e105f39752fcbf9dab471fed5d80e0aa229f1d0d Mon Sep 17 00:00:00 2001 From: Helium314 Date: Tue, 11 Feb 2025 23:21:34 +0100 Subject: [PATCH] add option to load colors --- .../keyboard/keyboard/KeyboardTheme.kt | 16 ++++ .../dialogs/ColorThemePickerDialog.kt | 84 +++++++++++++++++-- .../keyboard/settings/screens/ColorsScreen.kt | 8 +- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt index 1a154e915..7f6d6e30b 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt @@ -378,6 +378,22 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) { return colorMap } + fun getUnusedThemeName(initialName: String, prefs: SharedPreferences): String { + val existingNames = prefs.all.keys.mapNotNull { + when { + it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX) + it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX) + it.startsWith(Settings.PREF_USER_MORE_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_MORE_COLORS_PREFIX) + else -> null + } + }.toSortedSet() + if (initialName !in existingNames) return initialName + var i = 1 + while ("$initialName$i" in existingNames) + i++ + return "$initialName$i" + } + // returns false if not renamed due to invalid name or collision fun renameUserColors(from: String, to: String, prefs: SharedPreferences): Boolean { if (to.isBlank()) return false // don't want that diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt index 720e6abb1..e7a3d6840 100644 --- a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt @@ -1,6 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only package helium314.keyboard.settings.dialogs +import android.app.Activity +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -31,16 +38,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import helium314.keyboard.keyboard.ColorSetting import helium314.keyboard.keyboard.KeyboardTheme import helium314.keyboard.latin.R +import helium314.keyboard.latin.common.ColorType import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.decodeBase36 +import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.getStringResourceOrName import helium314.keyboard.latin.utils.prefs import helium314.keyboard.settings.Setting import helium314.keyboard.settings.SettingsDestination import helium314.keyboard.settings.keyboardNeedsReload +import helium314.keyboard.settings.screens.SaveThoseColors +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import java.util.EnumMap -// specialized variant of ListPickerDialog @Composable fun ColorThemePickerDialog( onDismissRequest: () -> Unit, @@ -71,16 +85,14 @@ fun ColorThemePickerDialog( val index = colors.indexOf(selectedColor) if (index != -1) state.scrollToItem(index, -state.layoutInfo.viewportSize.height / 3) } + var showLoadDialog by remember { mutableStateOf(false) } ThreeButtonAlertDialog( onDismissRequest = onDismissRequest, cancelButtonText = stringResource(R.string.dialog_close), onConfirmed = { }, confirmButtonText = null, -// neutralButtonText = stringResource(R.string.load), - onNeutral = { - // todo: launcher to select file - // when importing make sure name is not in use - }, + neutralButtonText = stringResource(R.string.load), + onNeutral = { showLoadDialog = true }, title = { Text(setting.title) }, text = { CompositionLocalProvider( @@ -173,6 +185,66 @@ fun ColorThemePickerDialog( } }, ) + if (showLoadDialog) { + var errorDialog by remember { mutableStateOf(false) } + val loadFilePicker = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = it.data?.data ?: return@rememberLauncherForActivityResult + ctx.getActivity()?.contentResolver?.openInputStream(uri)?.use { + errorDialog = loadColorString(it.reader().readText(), prefs) + } + } + ConfirmationDialog( + onDismissRequest = { showLoadDialog = false }, + onConfirmed = { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/*", "application/octet-stream", "application/json")) + .setType("*/*") + loadFilePicker.launch(intent) + }, + confirmButtonText = stringResource(R.string.button_load_custom), + onNeutral = { + val cm = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = cm.primaryClip?.takeIf { it.itemCount > 0 } ?: return@ConfirmationDialog + val text = clip.getItemAt(0).text + errorDialog = loadColorString(text.toString(), prefs) + }, + neutralButtonText = "load from clipboard" // todo: this is too long, maybe better if "load file" is changed to "load"? + ) + if (errorDialog) + InfoDialog("error") { showLoadDialog = false } // also error dialog to false? + } +} + +private fun loadColorString(colorString: String, prefs: SharedPreferences): Boolean { + try { + val that = Json.decodeFromString(colorString) + val themeName = KeyboardTheme.getUnusedThemeName(that.name ?: "imported colors", prefs) + val colors = that.colors.map { ColorSetting(it.key, it.value.second, it.value.first) } + KeyboardTheme.writeUserColors(prefs, themeName, colors) + KeyboardTheme.writeUserMoreColors(prefs, themeName, that.moreColors) + } catch (e: SerializationException) { + try { + val allColorsStringMap = Json.decodeFromString>(colorString) + val allColors = EnumMap(ColorType::class.java) + var themeName = "imported colors" + allColorsStringMap.forEach { + try { + allColors[ColorType.valueOf(it.key)] = it.value + } catch (_: IllegalArgumentException) { + themeName = decodeBase36(it.key) + } + } + themeName = KeyboardTheme.getUnusedThemeName(themeName, prefs) + KeyboardTheme.writeUserAllColors(prefs, themeName, allColors) + KeyboardTheme.writeUserMoreColors(prefs, themeName, 2) + } catch (e: SerializationException) { + return false + } + } + keyboardNeedsReload = true + return true } @Preview diff --git a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt index 07f6c0422..58273445f 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt @@ -73,12 +73,6 @@ fun ColorsScreen( isNight: Boolean, onClickBack: () -> Unit ) { - // todo: - // allow save (load should be in theme selector, maybe here too) - // import/export should now also store theme name - // handle name collisions on load by simply appending a number - // make sure import of old colors works - val ctx = LocalContext.current // is there really no better way of only setting forceOpposite while the screen is shown (and not paused)? @@ -163,7 +157,7 @@ fun ColorsScreen( stringResource(R.string.save) to { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) - .putExtra(Intent.EXTRA_TITLE,"theme.json") + .putExtra(Intent.EXTRA_TITLE,"${newThemeName.text}.json") .setType("application/json") saveLauncher.launch(intent) },