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.widget.TextView
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 helium314.keyboard.compat.locale
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.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.settings.dialogs.ConfirmationDialog
import java.io.File
import java.util.Locale
@ -97,6 +104,74 @@ fun createDictionaryTextHtml(message: String, locale: Locale, context: Context):
""".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) {
val dictionaryDir = File(DictionaryInfoUtils.getWordListCacheDirectory(context))
val dirs = dictionaryDir.listFiles() ?: return

View file

@ -8,6 +8,13 @@ import android.view.View
import android.widget.RelativeLayout
import androidx.activity.ComponentActivity
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 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. */
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.fillMaxWidth
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import helium314.keyboard.compat.locale
import helium314.keyboard.latin.Dictionary
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.LocaleUtils.localizedDisplayName
import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.createDictionaryTextAnnotated
import helium314.keyboard.settings.dictionaryFilePicker
import helium314.keyboard.settings.screens.getUserAndInternalDictionaries
import java.util.Locale
@ -46,7 +51,8 @@ fun DictionaryDialog(
cancelButtonText = stringResource(R.string.dialog_close),
title = { Text(locale.localizedDisplayName(ctx)) },
text = {
Column {
val state = rememberScrollState()
Column(Modifier.verticalScroll(state)) {
if (hasInternal) {
val color = if (dictionaries.none { it.startsWith(Dictionary.TYPE_MAIN + ":") }) MaterialTheme.colorScheme.onSurface
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 ?: ""))}
)
}
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),

View file

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

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.Dictionary
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.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.appendLink
import helium314.keyboard.latin.utils.getDictionaryLocales
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.prefs
@ -96,9 +98,14 @@ fun DictionaryScreen(
},
title = { Text(stringResource(R.string.add_new_dictionary_title)) },
text = {
// todo: no html in compose
val dictLink = "<a href='$DICTIONARY_URL'>" + ctx.getString(R.string.dictionary_link_text) + "</a>"
Text(stringResource(R.string.add_dictionary, dictLink))
// addDictString contains "%s" since we didn't supply a formatArg
val addDictString = stringResource(R.string.add_dictionary)
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.utils.DictionaryInfoUtils
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.SubtypeLocaleUtils
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.SettingsActivity
import helium314.keyboard.settings.dialogs.SubtypeDialog
import java.util.Locale
@Composable
fun LanguageScreen(
@ -82,6 +84,7 @@ fun LanguageScreen(
.clickable { selectedSubtype = item }
.padding(vertical = 6.dp, horizontal = 16.dp)
) {
var showNoDictDialog by remember { mutableStateOf(false) }
Column(modifier = Modifier.weight(1f)) {
Text(item.displayName(ctx), style = MaterialTheme.typography.bodyLarge)
val description = item.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)?.split(Separators.KV)
@ -96,10 +99,14 @@ fun LanguageScreen(
Switch(
checked = item in enabledSubtypes,
onCheckedChange = {
if (it && !dictsAvailable(item.locale(), ctx))
showNoDictDialog = true
if (it) SubtypeSettings.addEnabledSubtype(prefs, 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
private fun getSortedSubtypes(context: Context): List<InputMethodSubtype> {
val systemLocales = SubtypeSettings.getSystemLocales()