fix links using annotatedString instead of html

This commit is contained in:
Helium314 2025-02-24 16:49:03 +01:00
parent fa72e2bcbb
commit d5d672ee33
6 changed files with 138 additions and 9 deletions

View file

@ -7,6 +7,12 @@ import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.core.content.edit import androidx.core.content.edit
import helium314.keyboard.compat.locale import helium314.keyboard.compat.locale
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
@ -14,6 +20,7 @@ import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.settings.dialogs.ConfirmationDialog
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -97,6 +104,74 @@ fun createDictionaryTextHtml(message: String, locale: Locale, context: Context):
""".trimIndent() """.trimIndent()
} }
// why is this so horrible with annotated string?
@Composable
fun MissingDictionaryDialog(onDismissRequest: () -> Unit, locale: Locale) {
val prefs = LocalContext.current.prefs()
val availableDicts = createDictionaryTextAnnotated(locale)
val dictLink = "$DICTIONARY_URL/src/branch/main/dictionaries/main_$locale.dict"
val message = stringResource(R.string.no_dictionary_message, "§repl1§", locale.toString(), "§repl2§")
.replace("<br>", "\n") // compose doesn't understand html... // todo: modify the string?
// this relies on the order and thus is fragile, but so far it's fine with all translations
val part1 = message.substringBefore("§repl1§")
val part2 = message.substringBefore("§repl2§").substringAfter("§repl1§")
val part3 = message.substringAfter("§repl2§")
val annotatedString = buildAnnotatedString {
append(part1)
appendLink(stringResource(R.string.dictionary_link_text), DICTIONARY_URL)
append(part2)
appendLink(stringResource(R.string.dictionary_link_text), dictLink)
append(part3)
if (availableDicts.isNotEmpty()) {
appendLine()
appendLine()
append(availableDicts)
}
}
ConfirmationDialog(
onDismissRequest = onDismissRequest,
cancelButtonText = stringResource(R.string.dialog_close),
onConfirmed = { prefs.edit { putBoolean(Settings.PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG, true) } },
confirmButtonText = stringResource(R.string.no_dictionary_dont_show_again_button),
text = { Text(annotatedString) }
)
}
/** if dictionaries for [locale] or language are available returns links to them */
@Composable
fun createDictionaryTextAnnotated(locale: Locale): AnnotatedString {
val knownDicts = mutableListOf<Pair<String, String>>()
val builder = AnnotatedString.Builder()
builder.appendLine(stringResource(R.string.dictionary_available))
val context = LocalContext.current
context.assets.open("dictionaries_in_dict_repo.csv").reader().forEachLine {
if (it.isBlank()) return@forEachLine
val (type, localeString, experimental) = it.split(",")
// we use a locale string here because that's in the dictionaries repo
// ideally the repo would switch to language tag, but not sure how this is handled in the dictionary header
// further, the dicts in the dictionaries repo should be compatible with other AOSP-based keyboards
val dictLocale = localeString.constructLocale()
if (LocaleUtils.getMatchLevel(locale, dictLocale) < LocaleUtils.LOCALE_GOOD_MATCH) return@forEachLine
val rawDictString = "$type: ${dictLocale.getDisplayName(context.resources.configuration.locale())}"
val dictString = if (experimental.isEmpty()) rawDictString
else context.getString(R.string.available_dictionary_experimental, rawDictString)
val dictBaseUrl = DICTIONARY_URL + DICTIONARY_DOWNLOAD_SUFFIX +
if (experimental.isEmpty()) DICTIONARY_NORMAL_SUFFIX else DICTIONARY_EXPERIMENTAL_SUFFIX
val dictLink = dictBaseUrl + type + "_" + localeString.lowercase() + ".dict"
knownDicts.add(dictString to dictLink)
}
if (knownDicts.isEmpty()) return AnnotatedString("")
knownDicts.forEach {
builder.append("\u2022 ") // bullet point as replacement for <ul>
builder.appendLink(it.first , it.second)
builder.appendLine()
}
return builder.toAnnotatedString()
}
fun cleanUnusedMainDicts(context: Context) { fun cleanUnusedMainDicts(context: Context) {
val dictionaryDir = File(DictionaryInfoUtils.getWordListCacheDirectory(context)) val dictionaryDir = File(DictionaryInfoUtils.getWordListCacheDirectory(context))
val dirs = dictionaryDir.listFiles() ?: return val dirs = dictionaryDir.listFiles() ?: return

View file

@ -8,6 +8,13 @@ import android.view.View
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.withLink
import androidx.fragment.app.commit import androidx.fragment.app.commit
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
@ -88,3 +95,13 @@ fun Context.prefs(): SharedPreferences = DeviceProtectedUtils.getSharedPreferenc
/** The "default" preferences that are only accessible after the device has been unlocked. */ /** The "default" preferences that are only accessible after the device has been unlocked. */
fun Context.protectedPrefs(): SharedPreferences = getSharedPreferences("${packageName}_preferences", Context.MODE_PRIVATE) fun Context.protectedPrefs(): SharedPreferences = getSharedPreferences("${packageName}_preferences", Context.MODE_PRIVATE)
@Composable
fun AnnotatedString.Builder.appendLink(text: String, url: String) =
withLink(
LinkAnnotation.Url(
url,
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
)) {
append(text)
}

View file

@ -6,9 +6,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -22,11 +25,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import helium314.keyboard.compat.locale import helium314.keyboard.compat.locale
import helium314.keyboard.latin.Dictionary import helium314.keyboard.latin.Dictionary
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.LocaleUtils.localizedDisplayName import helium314.keyboard.latin.common.LocaleUtils.localizedDisplayName
import helium314.keyboard.latin.utils.DictionaryInfoUtils import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.createDictionaryTextAnnotated
import helium314.keyboard.settings.dictionaryFilePicker import helium314.keyboard.settings.dictionaryFilePicker
import helium314.keyboard.settings.screens.getUserAndInternalDictionaries import helium314.keyboard.settings.screens.getUserAndInternalDictionaries
import java.util.Locale import java.util.Locale
@ -46,7 +51,8 @@ fun DictionaryDialog(
cancelButtonText = stringResource(R.string.dialog_close), cancelButtonText = stringResource(R.string.dialog_close),
title = { Text(locale.localizedDisplayName(ctx)) }, title = { Text(locale.localizedDisplayName(ctx)) },
text = { text = {
Column { val state = rememberScrollState()
Column(Modifier.verticalScroll(state)) {
if (hasInternal) { if (hasInternal) {
val color = if (dictionaries.none { it.startsWith(Dictionary.TYPE_MAIN + ":") }) MaterialTheme.colorScheme.onSurface val color = if (dictionaries.none { it.startsWith(Dictionary.TYPE_MAIN + ":") }) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // for disabled look else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // for disabled look
@ -78,6 +84,11 @@ fun DictionaryDialog(
text = { Text(stringResource(R.string.remove_dictionary_message, type ?: ""))} text = { Text(stringResource(R.string.remove_dictionary_message, type ?: ""))}
) )
} }
val dictString = createDictionaryTextAnnotated(locale)
if (dictString.isNotEmpty()) {
HorizontalDivider()
Text(dictString, style = LocalTextStyle.current.merge(lineHeight = 1.8.em))
}
} }
}, },
neutralButtonText = stringResource(R.string.add_new_dictionary_title), neutralButtonText = stringResource(R.string.add_new_dictionary_title),

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import helium314.keyboard.keyboard.internal.KeyboardIconsSet import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_ALL import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_ALL
@ -60,6 +61,7 @@ import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtyp
import helium314.keyboard.latin.utils.SubtypeLocaleUtils import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeSettings import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.appendLink
import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.getDictionaryLocales import helium314.keyboard.latin.utils.getDictionaryLocales
import helium314.keyboard.latin.utils.getSecondaryLocales import helium314.keyboard.latin.utils.getSecondaryLocales
@ -379,14 +381,19 @@ private fun MainLayoutRow(
) )
} }
if (showAddLayoutDialog) { if (showAddLayoutDialog) {
// todo: maybe supply link to discussion section for layouts // layoutString contains "%s" since we didn't supply a formatArg
// todo: no html for compose, so message is broken val layoutString = stringResource(R.string.message_add_custom_layout)
// try annotatedString val linkText = stringResource(R.string.dictionary_link_text)
val link = "<a href='$LAYOUT_FORMAT_URL'>" + ctx.getString(R.string.dictionary_link_text) + "</a>" val annotated = buildAnnotatedString {
append(layoutString.substringBefore("%s"))
appendLink(linkText, LAYOUT_FORMAT_URL)
append(layoutString.substringAfter("%s"))
}
ConfirmationDialog( ConfirmationDialog(
onDismissRequest = { showAddLayoutDialog = false }, onDismissRequest = { showAddLayoutDialog = false },
title = { Text(stringResource(R.string.button_title_add_custom_layout)) }, title = { Text(stringResource(R.string.button_title_add_custom_layout)) },
text = { Text(stringResource(R.string.message_add_custom_layout, link)) }, text = { Text(annotated) },
onConfirmed = { showLayoutEditDialog = "new layout" to "" }, onConfirmed = { showLayoutEditDialog = "new layout" to "" },
neutralButtonText = stringResource(R.string.button_load_custom), neutralButtonText = stringResource(R.string.button_load_custom),
onNeutral = { onNeutral = {

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.Dictionary import helium314.keyboard.latin.Dictionary
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
@ -28,6 +29,7 @@ import helium314.keyboard.latin.utils.DICTIONARY_URL
import helium314.keyboard.latin.utils.DictionaryInfoUtils import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.SubtypeLocaleUtils import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeSettings import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.appendLink
import helium314.keyboard.latin.utils.getDictionaryLocales import helium314.keyboard.latin.utils.getDictionaryLocales
import helium314.keyboard.latin.utils.locale import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.prefs
@ -96,9 +98,14 @@ fun DictionaryScreen(
}, },
title = { Text(stringResource(R.string.add_new_dictionary_title)) }, title = { Text(stringResource(R.string.add_new_dictionary_title)) },
text = { text = {
// todo: no html in compose // addDictString contains "%s" since we didn't supply a formatArg
val dictLink = "<a href='$DICTIONARY_URL'>" + ctx.getString(R.string.dictionary_link_text) + "</a>" val addDictString = stringResource(R.string.add_dictionary)
Text(stringResource(R.string.add_dictionary, dictLink)) val annotated = buildAnnotatedString {
append(addDictString.substringBefore("%s"))
appendLink(stringResource(R.string.dictionary_link_text), DICTIONARY_URL)
append(addDictString.substringAfter("%s"))
}
Text(annotated)
} }
) )
} }

View file

@ -32,6 +32,7 @@ import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX
import helium314.keyboard.latin.utils.DictionaryInfoUtils import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.Log import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.MissingDictionaryDialog
import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.utils.SubtypeLocaleUtils import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeSettings import helium314.keyboard.latin.utils.SubtypeSettings
@ -43,6 +44,7 @@ import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.SearchScreen import helium314.keyboard.settings.SearchScreen
import helium314.keyboard.settings.SettingsActivity import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.dialogs.SubtypeDialog import helium314.keyboard.settings.dialogs.SubtypeDialog
import java.util.Locale
@Composable @Composable
fun LanguageScreen( fun LanguageScreen(
@ -82,6 +84,7 @@ fun LanguageScreen(
.clickable { selectedSubtype = item } .clickable { selectedSubtype = item }
.padding(vertical = 6.dp, horizontal = 16.dp) .padding(vertical = 6.dp, horizontal = 16.dp)
) { ) {
var showNoDictDialog by remember { mutableStateOf(false) }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(item.displayName(ctx), style = MaterialTheme.typography.bodyLarge) Text(item.displayName(ctx), style = MaterialTheme.typography.bodyLarge)
val description = item.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)?.split(Separators.KV) val description = item.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)?.split(Separators.KV)
@ -96,10 +99,14 @@ fun LanguageScreen(
Switch( Switch(
checked = item in enabledSubtypes, checked = item in enabledSubtypes,
onCheckedChange = { onCheckedChange = {
if (it && !dictsAvailable(item.locale(), ctx))
showNoDictDialog = true
if (it) SubtypeSettings.addEnabledSubtype(prefs, item) if (it) SubtypeSettings.addEnabledSubtype(prefs, item)
else SubtypeSettings.removeEnabledSubtype(ctx, item) else SubtypeSettings.removeEnabledSubtype(ctx, item)
} }
) )
if (showNoDictDialog)
MissingDictionaryDialog({ showNoDictDialog = false }, item.locale())
} }
} }
) )
@ -118,6 +125,11 @@ fun LanguageScreen(
} }
} }
private fun dictsAvailable(locale: Locale, context: Context): Boolean {
val (dicts, hasInternal) = getUserAndInternalDictionaries(context, locale)
return hasInternal || dicts.isNotEmpty()
}
// sorting by display name is still slow, even with the cache... but probably good enough // sorting by display name is still slow, even with the cache... but probably good enough
private fun getSortedSubtypes(context: Context): List<InputMethodSubtype> { private fun getSortedSubtypes(context: Context): List<InputMethodSubtype> {
val systemLocales = SubtypeSettings.getSystemLocales() val systemLocales = SubtypeSettings.getSystemLocales()