diff --git a/README.md b/README.md index 04cb2158..6156effe 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Does not use internet permission, and thus is 100% offline. - [Contributing](#contributing-) * [Reporting Issues](#reporting-issues) * [Translations](#translations) - * [Dictionary Creation](#dictionary-creation) + * [To Community Creation](#to-community) * [Code Contribution](CONTRIBUTING.md) - [To-do](#to-do) - [License](#license) @@ -113,9 +113,17 @@ If you're interested, you can read the following useful text about effective bug Translations can be added using [Weblate](https://translate.codeberg.org/projects/heliboard/). You will need an account to update translations and add languages. Add the language you want to translate to in Languages -> Manage translated languages in the top menu bar. Updating translations in a PR will not be accepted, as it may cause conflicts with Weblate translations. -## Dictionary Creation -There will not be any further dictionaries bundled in this app. However, you can add dictionaries to the [dictionaries repository](https://codeberg.org/Helium314/aosp-dictionaries). -To create or update a dictionary for your language, you can use [this tool](https://github.com/remi0s/aosp-dictionary-tools). You will need a wordlist, as described [here](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/wordlists/sample.combined) and in the repository readme. +## To Community +You can share your themes, layouts and dictionaries with other people: +* Themes can be saved and loaded using the menu on top-right in the _adjust colors_ screen +* Custom keyboard layouts are text files whose content you can edit, copy and share + * this applies to main keyboard layouts and to special layouts adjustable in advanced settings + * see [layouts.md](layouts.md) for details +* Creating dictionaries is a little more work + * first you will need a wordlist, as described [here](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/wordlists/sample.combined) and in the repository readme + * the you need to compile the dictionary using [external tools](https://github.com/remi0s/aosp-dictionary-tools) + * the resulting file (and ideally the wordlist too) can be shared with other users + * note that there will not be any further dictionaries added to this app, but you can add dictionaries to the [dictionaries repository](https://codeberg.org/Helium314/aosp-dictionaries) ## Code Contribution See [Contribution Guidelines](CONTRIBUTING.md) @@ -133,8 +141,6 @@ __Planned features and improvements:__ * Add and enable emoji dictionaries by default (if available for language) * Clearer / more intuitive arrangement of settings * Maybe hide some less used settings by default (similar to color customization) -* Customizable currency keys -* Ability to export/import (share) custom colors * Make use of the `.com` key in URL fields (currently only available for tablets) * With language-dependent TLDs * Internal cleanup (a lot of over-complicated and convoluted code) diff --git a/app/src/main/java/helium314/keyboard/latin/settings/ColorsSettingsFragment.kt b/app/src/main/java/helium314/keyboard/latin/settings/ColorsSettingsFragment.kt index a62e53d5..25752aae 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/ColorsSettingsFragment.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/ColorsSettingsFragment.kt @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only package helium314.keyboard.latin.settings +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent import android.content.res.Configuration import android.os.Bundle import android.view.Menu @@ -9,7 +14,11 @@ import android.view.MenuItem import android.view.View import android.view.WindowManager import android.widget.CompoundButton +import android.widget.EditText import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit @@ -17,6 +26,7 @@ import androidx.core.view.MenuProvider import androidx.core.view.forEach import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import com.rarepebble.colorpicker.ColorPickerView import helium314.keyboard.keyboard.KeyboardSwitcher @@ -25,12 +35,19 @@ import helium314.keyboard.latin.RichInputMethodManager import helium314.keyboard.latin.common.ColorType import helium314.keyboard.latin.common.default import helium314.keyboard.latin.common.readAllColorsMap +import helium314.keyboard.latin.common.splitOnWhitespace import helium314.keyboard.latin.common.writeAllColorsMap import helium314.keyboard.latin.databinding.ColorSettingBinding import helium314.keyboard.latin.databinding.ColorSettingsBinding import helium314.keyboard.latin.utils.DeviceProtectedUtils import helium314.keyboard.latin.utils.ExecutorUtils import helium314.keyboard.latin.utils.ResourceUtils +import helium314.keyboard.latin.utils.infoDialog +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.EnumMap open class ColorsSettingsFragment : Fragment(R.layout.color_settings), MenuProvider { @@ -96,6 +113,8 @@ open class ColorsSettingsFragment : Fragment(R.layout.color_settings), MenuProvi menu.add(Menu.NONE, 0, Menu.NONE, R.string.main_colors) menu.add(Menu.NONE, 1, Menu.NONE, R.string.more_colors) menu.add(Menu.NONE, 2, Menu.NONE, R.string.all_colors) + menu.add(Menu.NONE, 3, Menu.NONE, R.string.save) + menu.add(Menu.NONE, 4, Menu.NONE, R.string.load) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { @@ -111,6 +130,14 @@ open class ColorsSettingsFragment : Fragment(R.layout.color_settings), MenuProvi updateColorPrefs() return true } + if (menuItem.itemId == 3) { + saveDialog() + return true + } + if (menuItem.itemId == 4) { + loadDialog() + return true + } return false } @@ -298,6 +325,117 @@ open class ColorsSettingsFragment : Fragment(R.layout.color_settings), MenuProvi companion object { var forceOppositeTheme = false } + + // ----------------- stuff for import / export --------------------------- + @Serializable + private data class SaveThoseColors(val moreColors: Int, val colors: Map>) + + private fun saveDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.save) + .setPositiveButton(R.string.button_save_file) { _, _ -> + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .putExtra(Intent.EXTRA_TITLE,"theme.json") + .setType("application/json") + saveFilePicker.launch(intent) + } + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(R.string.copy_to_clipboard) { _, _ -> + val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("HeliBoard theme", getColorString())) + } + .show() + } + + private fun loadDialog() { + val layout = LinearLayout(requireContext()) + layout.orientation = LinearLayout.VERTICAL + layout.addView(TextView(requireContext()).apply { setText(R.string.load_will_overwrite) }) + val et = EditText(requireContext()) + layout.addView(et) + val padding = ResourceUtils.toPx(8, resources) + layout.setPadding(3 * padding, padding, padding, padding) + val d = AlertDialog.Builder(requireContext()) + .setTitle(R.string.load) + .setView(layout) + .setPositiveButton(android.R.string.ok) { _, _ -> + loadColorString(et.text.toString()) + } + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(R.string.button_load_custom) { _, _ -> + 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) + } + .create() + et.doAfterTextChanged { d.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = et.text.toString().isNotBlank() } + d.show() + d.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = false + } + + private fun loadColorString(colorString: String) { + // show dialog + // load from file or from text field + // do some sanity check (only write correct settings, consider current night mode) + try { + val that = Json.decodeFromString(colorString) + // save mode to moreColors and PREF_SHOW_MORE_COLORS (with night dependence!) + that.colors.forEach { + val pref = Settings.getColorPref(it.key, isNight) + if (it.value.first == null) + prefs.edit { remove(pref) } + else prefs.edit { putInt(pref, it.value.first!!) } + prefs.edit { putBoolean(pref + Settings.PREF_AUTO_USER_COLOR_SUFFIX, it.value.second) } + } + moreColors = that.moreColors + } catch (e: SerializationException) { + try { + val allColorsStringMap = Json.decodeFromString>(colorString) + val allColors = EnumMap(ColorType::class.java) + allColorsStringMap.forEach { + try { + allColors[ColorType.valueOf(it.key)] = it.value + } catch (_: IllegalArgumentException) {} + } + writeAllColorsMap(allColors, prefs, isNight) + moreColors = 2 + } catch (e: SerializationException) { + infoDialog(requireContext(), "error") + } + } + updateColorPrefs() + KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext()) + } + + private fun getColorString(): String { + if (moreColors == 2) + return Json.encodeToString(readAllColorsMap(prefs, isNight).map { it.key.name to it.value }.toMap()) + // read the actual prefs! + val colors = colorPrefsAndNames.associate { + val pref = Settings.getColorPref(it.first, isNight) + val color = if (prefs.contains(pref)) prefs.getInt(pref, 0) else null + it.first to (color to prefs.getBoolean(pref + Settings.PREF_AUTO_USER_COLOR_SUFFIX, true)) + } + return Json.encodeToString(SaveThoseColors(moreColors, colors)) + } + + private val saveFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult + val uri = it.data?.data ?: return@registerForActivityResult + activity?.contentResolver?.openOutputStream(uri)?.writer()?.use { it.write(getColorString()) } + } + + private val loadFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult + val uri = it.data?.data ?: return@registerForActivityResult + activity?.contentResolver?.openInputStream(uri)?.use { + loadColorString(it.reader().readText()) + } ?: infoDialog(requireContext(), R.string.file_read_error) + } + } class ColorsNightSettingsFragment : ColorsSettingsFragment() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15a2a8ea..416f63b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -451,6 +451,8 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM Remove Save + + Load Language @@ -459,8 +461,12 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM Add custom layout Select a file in a compatible format. Information about the formats is available %s. - + Load file + + Save to file + + Copy to Clipboard Cannot read file @@ -721,6 +727,8 @@ New dictionary: Show more colors Show all colors + + Loading will overwrite the current theme This setting exposes all colors that are used internally. The list of colors may change at any time. The default color is random, and the names will not be translated.