add ability to save / load custom themes

start for #852
This commit is contained in:
Helium314 2024-06-30 19:15:56 +02:00
parent 7dac19cec7
commit 74571a3202
3 changed files with 159 additions and 7 deletions

View file

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

View file

@ -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<String, Pair<Int?, Boolean>>)
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<SaveThoseColors>(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<Map<String, Int>>(colorString)
val allColors = EnumMap<ColorType, Int>(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() {

View file

@ -451,6 +451,8 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
<string name="remove">Remove</string>
<!-- Title of the button to save a custom style entry in the settings dialog [CHAR LIMIT=15] -->
<string name="save">Save</string>
<!-- Title of the button to load a custom style entry in the settings dialog [CHAR LIMIT=15] -->
<string name="load">Load</string>
<!-- Title of the spinner for choosing a language of custom style in the settings dialog [CHAR LIMIT=15] -->
<string name="subtype_locale">Language</string>
<!-- Title of the section for choosing a keyboard layout language settings dialog -->
@ -459,8 +461,12 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
<string name="button_title_add_custom_layout">Add custom layout</string>
<!-- Message text for adding a custom layout, interpreted as HTML -->
<string name="message_add_custom_layout">Select a file in a compatible format. Information about the formats is available %s.</string>
<!-- Button text for loading a custom layout or image file -->
<!-- Button text for loading a custom layout, image or theme file -->
<string name="button_load_custom">Load file</string>
<!-- Button text for saving a theme file -->
<string name="button_save_file">Save to file</string>
<!-- Button text for copying text to clipboard -->
<string name="copy_to_clipboard">Copy to Clipboard</string>
<!-- Error message when file cannot be read -->
<string name="file_read_error">Cannot read file</string>
<!-- Button text for copying an existing layout -->
@ -721,6 +727,8 @@ New dictionary:
<string name="more_colors">Show more colors</string>
<!-- Menu item for showing all colors -->
<string name="all_colors">Show all colors</string>
<!-- Warning message displayed when loading theme colors -->
<string name="load_will_overwrite">Loading will overwrite the current theme</string>
<!-- Warning message displayed when showing all colors -->
<string name="all_colors_warning">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.</string>
<!-- Hint for text field just to show keyboard (in color settings) -->