Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/assets/emoji.json

Large diffs are not rendered by default.

143 changes: 143 additions & 0 deletions app/src/main/assets/symbol_shortcodes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{
"euro": "€",
"pound": "£",
"yen": "¥",
"cent": "¢",
"rupee": "₹",
"dollar": "$",
"percent": "%",
"pct": "%",
"permille": "‰",
"amp": "&",
"ampersand": "&",
"at": "@",
"hash": "#",
"hashtag": "#",
"caret": "^",
"tilde": "~",
"backslash": "\\",
"slash": "/",
"pipe": "|",
"underscore": "_",
"grave": "`",
"apostrophe": "'",
"comma": ",",
"period": ".",
"semicolon": ";",
"colon": ":",
"exclamation": "!",
"question": "?",
"lparen": "(",
"rparen": ")",
"lbrace": "{",
"rbrace": "}",
"lbracket": "[",
"rbracket": "]",
"less": "<",
"greater": ">",
"equal": "=",
"plus": "+",
"minus": "-",
"asterisk": "*",
"times": "×",
"multiply": "×",
"divide": "÷",
"division": "÷",
"plusminus": "±",
"pm": "±",
"infinity": "∞",
"inf": "∞",
"approximately": "≈",
"approx": "≈",
"notequal": "≠",
"neq": "≠",
"lessequal": "≤",
"leq": "≤",
"greaterequal": "≥",
"geq": "≥",
"sum": "∑",
"product": "∏",
"integral": "∫",
"sqrt": "√",
"cuberoot": "∛",
"fourthroot": "∜",
"bullet": "•",
"middot": "·",
"ellipsis": "…",
"dots": "…",
"section": "§",
"paragraph": "¶",
"dagger": "†",
"doubledagger": "‡",
"degree": "°",
"deg": "°",
"micro": "µ",
"alpha": "α",
"beta": "β",
"gamma": "γ",
"delta": "δ",
"epsilon": "ε",
"theta": "θ",
"lambda": "λ",
"mu": "μ",
"sigma": "σ",
"omega": "ω",
"pi": "π",
"tau": "τ",
"phi": "φ",
"arrow": "→",
"rightarrow": "→",
"leftarrow": "←",
"uparrow": "↑",
"downarrow": "↓",
"leftrightarrow": "↔",
"doublearrow": "⇒",
"implies": "⇒",
"star": "★",
"blackstar": "★",
"whitestar": "☆",
"heart": "♥",
"blackheart": "♥",
"whiteheart": "♡",
"spade": "♠",
"club": "♣",
"diamond": "♦",
"check": "✓",
"checkmark": "✓",
"cross": "✗",
"xmark": "✗",
"music": "♪",
"note": "♪",
"notes": "♫",
"sun": "☀",
"cloud": "☁",
"umbrella": "☂",
"snowflake": "❄",
"telephone": "☎",
"phone": "☎",
"mail": "✉",
"email": "✉",
"pencil": "✎",
"scissors": "✂",
"airplane": "✈",
"warning": "⚠",
"peace": "☮",
"yin": "☯",
"yinyang": "☯",
"fraction14": "¼",
"quarter": "¼",
"fraction12": "½",
"half": "½",
"fraction34": "¾",
"super2": "²",
"squared": "²",
"super3": "³",
"cubed": "³",
"laquo": "«",
"raquo": "»",
"ldquo": ",\n \"rdquo\": ",
"lsquo": "'",
"rsquo": "'",
"mdash": "—",
"ndash": "–"
}
24 changes: 24 additions & 0 deletions app/src/main/java/it/palsoftware/pastiera/SettingsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ object SettingsManager {
private const val KEY_AUTO_CORRECT_ENABLED = "auto_correct_enabled"
private const val KEY_AUTO_CORRECT_ENABLED_LANGUAGES = "auto_correct_enabled_languages"
private const val KEY_SUGGESTIONS_ENABLED = "suggestions_enabled"
private const val KEY_EMOJI_SHORTCODE_ENABLED = "emoji_shortcode_enabled"
private const val KEY_SYMBOL_SHORTCODE_ENABLED = "symbol_shortcode_enabled"
private const val KEY_ACCENT_MATCHING_ENABLED = "accent_matching_enabled"
private const val KEY_AUTO_REPLACE_ON_SPACE_ENTER = "auto_replace_on_space_enter"
private const val KEY_MAX_AUTO_REPLACE_DISTANCE = "max_auto_replace_distance"
Expand Down Expand Up @@ -279,6 +281,8 @@ object SettingsManager {
private const val DEFAULT_LAYOUT_AWARE_CTRL_SHORTCUTS = false
private const val DEFAULT_AUTO_CORRECT_ENABLED = true
private const val DEFAULT_SUGGESTIONS_ENABLED = true
private const val DEFAULT_EMOJI_SHORTCODE_ENABLED = true
private const val DEFAULT_SYMBOL_SHORTCODE_ENABLED = true
private const val DEFAULT_ACCENT_MATCHING_ENABLED = true
private const val DEFAULT_AUTO_REPLACE_ON_SPACE_ENTER = false
private const val DEFAULT_MAX_AUTO_REPLACE_DISTANCE = 1
Expand Down Expand Up @@ -2531,6 +2535,26 @@ object SettingsManager {
.apply()
}

fun getEmojiShortcodeEnabled(context: Context): Boolean {
return getPreferences(context).getBoolean(KEY_EMOJI_SHORTCODE_ENABLED, DEFAULT_EMOJI_SHORTCODE_ENABLED)
}

fun setEmojiShortcodeEnabled(context: Context, enabled: Boolean) {
getPreferences(context).edit()
.putBoolean(KEY_EMOJI_SHORTCODE_ENABLED, enabled)
.apply()
}

fun getSymbolShortcodeEnabled(context: Context): Boolean {
return getPreferences(context).getBoolean(KEY_SYMBOL_SHORTCODE_ENABLED, DEFAULT_SYMBOL_SHORTCODE_ENABLED)
}

fun setSymbolShortcodeEnabled(context: Context, enabled: Boolean) {
getPreferences(context).edit()
.putBoolean(KEY_SYMBOL_SHORTCODE_ENABLED, enabled)
.apply()
}

/**
* Returns whether accent matching should be applied to suggestions and auto-replace.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ fun TextInputSettingsScreen(
onNavModeSettingsClick: () -> Unit = {}
) {
val context = LocalContext.current
var emojiShortcodeEnabled by remember {
mutableStateOf(SettingsManager.getEmojiShortcodeEnabled(context))
}
var symbolShortcodeEnabled by remember {
mutableStateOf(SettingsManager.getSymbolShortcodeEnabled(context))
}

var autoCapitalizeFirstLetter by remember {
mutableStateOf(SettingsManager.getAutoCapitalizeFirstLetter(context))
Expand Down Expand Up @@ -231,6 +237,26 @@ fun TextInputSettingsScreen(
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
SettingsSectionHeader(text = stringResource(R.string.shortcodes_title))
SettingsSwitchRow(
title = stringResource(R.string.emoji_shortcodes_title),
description = stringResource(R.string.emoji_shortcodes_description),
checked = emojiShortcodeEnabled,
onCheckedChange = { enabled ->
emojiShortcodeEnabled = enabled
SettingsManager.setEmojiShortcodeEnabled(context, enabled)
}
)
SettingsSwitchRow(
title = stringResource(R.string.symbol_shortcodes_title),
description = stringResource(R.string.symbol_shortcodes_description),
checked = symbolShortcodeEnabled,
onCheckedChange = { enabled ->
symbolShortcodeEnabled = enabled
SettingsManager.setSymbolShortcodeEnabled(context, enabled)
}
)

SettingsSectionHeader(text = stringResource(R.string.text_input_section_capitalization))
SettingsSwitchRow(
title = stringResource(R.string.auto_capitalize_title),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ object PreferenceSchemas {
"auto_correct_enabled" to PreferenceValueType.BOOLEAN,
"auto_correct_enabled_languages" to PreferenceValueType.STRING,
"suggestions_enabled" to PreferenceValueType.BOOLEAN,
"emoji_shortcode_enabled" to PreferenceValueType.BOOLEAN,
"symbol_shortcode_enabled" to PreferenceValueType.BOOLEAN,
"accent_matching_enabled" to PreferenceValueType.BOOLEAN,
"auto_replace_on_space_enter" to PreferenceValueType.BOOLEAN,
"auto_capitalize_after_period" to PreferenceValueType.BOOLEAN,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package it.palsoftware.pastiera.emoji

import android.content.Context
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject

class EmojiShortcodeManager(private val context: Context) {

companion object {
private const val TAG = "EmojiShortcode"
private const val EMOJI_DATA_FILE = "emoji.json"
private const val SYMBOL_SHORTCODES_FILE = "symbol_shortcodes.json"
private val SHORTCODE_REGEX = Regex("[a-zA-Z0-9_]+")
private val COMPLETED_SHORTCODE_REGEX = Regex("(^|[\\s\\(\\[\\{\"'])[:]([a-zA-Z0-9_]{1,30}):$")
}

private val shortcodeMap = mutableMapOf<String, String>()

init {
loadShortcodes()
}

private fun hexToEmoji(hex: String): String {
val codePoints = hex.split("-").map { it.toInt(16) }
val chars = codePoints.flatMap { Character.toChars(it).toList() }.toCharArray()
return String(chars)
}

private fun loadShortcodes() {
try {
val json = context.assets.open(EMOJI_DATA_FILE).bufferedReader().use { it.readText() }
val emojiArray = JSONArray(json)
for (index in 0 until emojiArray.length()) {
val emojiObject = emojiArray.getJSONObject(index)
val emoji = hexToEmoji(emojiObject.getString("unified"))
val shortNames = emojiObject.optJSONArray("short_names")
if (shortNames != null) {
for (shortNameIndex in 0 until shortNames.length()) {
shortcodeMap[shortNames.getString(shortNameIndex).lowercase()] = emoji
}
} else {
val shortName = emojiObject.optString("short_name", "")
if (shortName.isNotEmpty()) {
shortcodeMap[shortName.lowercase()] = emoji
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load emoji shortcodes", e)
}

try {
val json = context.assets.open(SYMBOL_SHORTCODES_FILE).bufferedReader().use { it.readText() }
val symbols = JSONObject(json)
val keys = symbols.keys()
while (keys.hasNext()) {
val shortcode = keys.next()
shortcodeMap[shortcode.lowercase()] = symbols.getString(shortcode)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load symbol shortcodes", e)
}
}

fun searchShortcodes(prefix: String, limit: Int = 10): List<Pair<String, String>> {
if (prefix.isEmpty()) return emptyList()
val normalizedPrefix = prefix.lowercase()
return shortcodeMap
.filter { it.key.startsWith(normalizedPrefix) }
.toList()
.sortedBy { it.first.length }
.take(limit)
.map { (shortcode, character) -> character to shortcode }
}

fun extractCurrentShortcode(textBeforeCursor: String): Pair<String, Int>? {
if (textBeforeCursor.isEmpty()) return null
val lastColonIndex = textBeforeCursor.lastIndexOf(':')
if (lastColonIndex == -1) return null
val afterColon = textBeforeCursor.substring(lastColonIndex + 1)
if (!afterColon.matches(SHORTCODE_REGEX) || afterColon.length > 30) {
return null
}
return afterColon to lastColonIndex
}

fun extractCompletedShortcode(textBeforeCursor: String): String? {
if (textBeforeCursor.isEmpty() || !textBeforeCursor.endsWith(':')) return null
val match = COMPLETED_SHORTCODE_REGEX.find(textBeforeCursor) ?: return null
return match.groupValues.getOrNull(2)?.lowercase()
}

fun lookupExactShortcode(shortcode: String): String? {
if (shortcode.isBlank()) return null
return shortcodeMap[shortcode.lowercase()]
}
}
Loading