Display imported dictionary name

This commit is contained in:
Aleksandras Kostarevas 2024-04-02 11:25:22 -05:00
parent 31b5fe933e
commit c4bd35c921
2 changed files with 123 additions and 43 deletions

View File

@ -39,7 +39,6 @@ import org.futo.inputmethod.latin.LatinIMELegacy
import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.ReadOnlyBinaryDictionary import org.futo.inputmethod.latin.ReadOnlyBinaryDictionary
import org.futo.inputmethod.latin.RichInputMethodManager import org.futo.inputmethod.latin.RichInputMethodManager
import org.futo.inputmethod.latin.uix.settings.DataStoreItem
import org.futo.inputmethod.latin.uix.settings.NavigationItem import org.futo.inputmethod.latin.uix.settings.NavigationItem
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
import org.futo.inputmethod.latin.uix.settings.ScreenTitle import org.futo.inputmethod.latin.uix.settings.ScreenTitle
@ -56,6 +55,7 @@ import org.futo.voiceinput.shared.BUILTIN_ENGLISH_MODEL
import org.futo.voiceinput.shared.types.ModelFileFile import org.futo.voiceinput.shared.types.ModelFileFile
import org.futo.voiceinput.shared.types.ModelLoader import org.futo.voiceinput.shared.types.ModelLoader
import java.io.File import java.io.File
import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.Locale import java.util.Locale
@ -77,18 +77,18 @@ fun getActiveLanguages(context: Context): List<InputLanguage> {
} }
fun FileKind.preferencesKeyFor(locale: String): Preferences.Key<String> { fun FileKind.preferenceKeyFor(locale: String): Preferences.Key<String> {
assert(this != FileKind.Invalid) assert(this != FileKind.Invalid)
return stringPreferencesKey("resource_${name}_${locale}") return stringPreferencesKey("resource_${name}_${locale}")
} }
@Composable fun FileKind.namePreferenceKeyFor(locale: String): Preferences.Key<String> {
fun resourceOption(language: InputMethodSubtype, kind: FileKind): DataStoreItem<String> { assert(this != FileKind.Invalid)
return useDataStore(key = kind.preferencesKeyFor(language.locale), default = "") return stringPreferencesKey("resourcename_${name}_${locale}")
} }
@Composable @Composable
fun ImportScreen(fileKind: FileKindAndInfo, file: String?, onApply: (FileKind, InputMethodSubtype) -> Unit, onCancel: () -> Unit) { fun ImportScreen(fileKind: FileKindAndInfo, file: String?, onApply: (FileKindAndInfo, InputMethodSubtype) -> Unit, onCancel: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val importing = remember { mutableStateOf(false) } val importing = remember { mutableStateOf(false) }
val importingLanguage = remember { mutableStateOf("") } val importingLanguage = remember { mutableStateOf("") }
@ -106,7 +106,7 @@ fun ImportScreen(fileKind: FileKindAndInfo, file: String?, onApply: (FileKind, I
} }
) )
} else { } else {
Text("You are importing a ${fileKind.kind.youAreImporting()}.", modifier = Modifier.padding(8.dp)) Text("You are importing a ${fileKind.kind.youAreImporting()}. ${fileKind.name?.let { "Info: $it" } ?: ""}", modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -124,10 +124,10 @@ fun ImportScreen(fileKind: FileKindAndInfo, file: String?, onApply: (FileKind, I
) )
val languages = getActiveLanguages(context).let { val languages = getActiveLanguages(context).let {
if(fileKind.guessedLanguage != null) { if(fileKind.locale != null) {
it.filter { it.tag.lowercase() == fileKind.guessedLanguage.lowercase() || it.tag.split("_")[0].lowercase() == fileKind.guessedLanguage.split("_")[0].lowercase() }.let { it.filter { it.tag.lowercase() == fileKind.locale.lowercase() || it.tag.split("_")[0].lowercase() == fileKind.locale.split("_")[0].lowercase() }.let {
if(it.isEmpty()) { if(it.isEmpty()) {
Text("Warning: This file appears to be intended for a language (${fileKind.guessedLanguage}) which is not active", modifier = Modifier.padding(8.dp)) Text("Warning: This file appears to be intended for a language (${fileKind.locale}) which is not active", modifier = Modifier.padding(8.dp))
getActiveLanguages(context) getActiveLanguages(context)
} else { } else {
it it
@ -145,7 +145,7 @@ fun ImportScreen(fileKind: FileKindAndInfo, file: String?, onApply: (FileKind, I
navigate = { navigate = {
importing.value = true importing.value = true
importingLanguage.value = it.name importingLanguage.value = it.name
onApply(fileKind.kind, it.inputMethodSubtype) onApply(fileKind, it.inputMethodSubtype)
} }
) )
} }
@ -181,9 +181,77 @@ fun FileKind.extension(): String {
data class FileKindAndInfo( data class FileKindAndInfo(
val kind: FileKind, val kind: FileKind,
val guessedLanguage: String? val name: String?,
val locale: String?
) )
private fun parseDictionaryMetadataKV(inputStream: InputStream): Map<String, String>? {
while (inputStream.available() > 0) {
val v = inputStream.read()
if(v == -1) {
return null
} else if (v == 'd'.code) {
if (inputStream.read() == 'a'.code
&& inputStream.read() == 't'.code
&& inputStream.read() == 'e'.code
&& inputStream.read() == 0x1F
) {
break
} else {
continue
}
}
}
val readUntilSeparator = {
val codes: MutableList<Int> = mutableListOf()
while(true) {
val v = inputStream.read()
if(v == -1) {
break
} else if(v == 0x1F) {
break
} else if(v == 0) {
// 3 byte character
// not 100% sure it's correct to compare to 0 here, but seems to work usually
val v1 = v
val v2 = inputStream.read()
val v3 = inputStream.read()
// sanity check
if(v2 == -1 || v3 == -1 || v2 == 0x1F || v3 == 0x1F) break
codes.add((v1 shl 16) or (v2 shl 8) or (v3))
} else {
codes.add(v)
}
}
String(codes.toIntArray(), 0, codes.size)
}
val keyValueList = mutableMapOf(
"date" to readUntilSeparator()
)
while(true) {
val key = readUntilSeparator()
val value = readUntilSeparator()
if(key.isBlank() || value.isBlank()) {
break
}
keyValueList[key] = value
if(key == "version") {
break
}
}
return keyValueList
}
fun determineFileKind(context: Context, file: Uri): FileKindAndInfo { fun determineFileKind(context: Context, file: Uri): FileKindAndInfo {
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
@ -198,42 +266,42 @@ fun determineFileKind(context: Context, file: Uri): FileKindAndInfo {
val magic = ByteBuffer.wrap(array).getInt().toUInt() val magic = ByteBuffer.wrap(array).getInt().toUInt()
when(magic) { when(magic) {
voiceInputMagic -> FileKindAndInfo(FileKind.VoiceInput, null) voiceInputMagic -> FileKindAndInfo(FileKind.VoiceInput, null, null)
transformerMagic -> FileKindAndInfo(FileKind.Transformer, null) transformerMagic -> FileKindAndInfo(FileKind.Transformer, null, null)
dictionaryMagic -> { dictionaryMagic -> {
while(array[0] != 0x3A.toByte()) { val metadata = parseDictionaryMetadataKV(inputStream)
inputStream.read(array, 0, 1)
}
val chars: MutableList<Char> = mutableListOf() FileKindAndInfo(
while(array[0] != 0x1F.toByte()) { FileKind.Dictionary,
inputStream.read(array, 0, 1) name = metadata?.get("description"),
if(array[0] == 0x1F.toByte()) break locale = metadata?.get("locale")
)
chars.add(array[0].toInt().toChar())
}
val language = String(chars.toCharArray())
FileKindAndInfo(FileKind.Dictionary, language)
} }
else -> FileKindAndInfo(FileKind.Invalid, null) else -> FileKindAndInfo(FileKind.Invalid, null, null)
} }
} ?: FileKindAndInfo(FileKind.Invalid, null) } ?: FileKindAndInfo(FileKind.Invalid, null, null)
} }
object ResourceHelper { object ResourceHelper {
suspend fun findFileForKind(context: Context, locale: Locale, kind: FileKind): File? { suspend fun findKeyForLocaleAndKind(context: Context, locale: Locale, kind: FileKind): String? {
val keysToTry = listOf( val keysToTry = listOf(
locale.language, locale.language,
"${locale.language}_${locale.country}", "${locale.language}_${locale.country}",
"${locale.language.lowercase()}_${locale.country.uppercase()}", "${locale.language.lowercase()}_${locale.country.uppercase()}",
) )
val settingValue: String = keysToTry.firstNotNullOfOrNull { key -> val key: String = keysToTry.firstNotNullOfOrNull { key ->
context.getSetting(kind.preferencesKeyFor(key), "").ifEmpty { null } context.getSetting(kind.preferenceKeyFor(key), "").ifEmpty { null }?.let { key }
} ?: return null } ?: return null
return key
}
suspend fun findFileForKind(context: Context, locale: Locale, kind: FileKind): File? {
val key = findKeyForLocaleAndKind(context, locale, kind) ?: return null
val settingValue: String = context.getSetting(kind.preferenceKeyFor(key), "")
val file = File(context.getExternalFilesDir(null), settingValue) val file = File(context.getExternalFilesDir(null), settingValue)
if(!file.exists()) { if(!file.exists()) {
@ -268,14 +336,16 @@ object ResourceHelper {
} }
fun deleteResourceForLanguage(context: Context, kind: FileKind, locale: Locale) { fun deleteResourceForLanguage(context: Context, kind: FileKind, locale: Locale) {
val setting = kind.preferencesKeyFor(locale.toString()) val setting = kind.preferenceKeyFor(locale.toString())
val value = runBlocking { context.getSetting(setting, "") } val value = runBlocking { context.getSetting(setting, "") }
if(value.isNotBlank()) { if(value.isNotBlank()) {
runBlocking { context.setSetting(setting, "") }
val file = File(context.getExternalFilesDir(null), value) val file = File(context.getExternalFilesDir(null), value)
file.delete() file.delete()
} }
runBlocking { context.setSetting(kind.preferenceKeyFor(locale.toString()), "") }
runBlocking { context.setSetting(kind.namePreferenceKeyFor(locale.toString()), "") }
LatinIMELegacy.mPendingDictionaryUpdate = true LatinIMELegacy.mPendingDictionaryUpdate = true
} }
} }
@ -283,16 +353,16 @@ object ResourceHelper {
class ImportResourceActivity : ComponentActivity() { class ImportResourceActivity : ComponentActivity() {
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null) private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
private val fileBeingImported: MutableState<String?> = mutableStateOf(null) private val fileBeingImported: MutableState<String?> = mutableStateOf(null)
private val fileKind: MutableState<FileKindAndInfo> = mutableStateOf(FileKindAndInfo(FileKind.Invalid, null)) private val fileKind: MutableState<FileKindAndInfo> = mutableStateOf(FileKindAndInfo(FileKind.Invalid, null, null))
private var uri: Uri? = null private var uri: Uri? = null
private fun applySetting(fileKind: FileKind, inputMethodSubtype: InputMethodSubtype) { private fun applySetting(fileKind: FileKindAndInfo, inputMethodSubtype: InputMethodSubtype) {
val outputFileName = "${fileKind.name.lowercase()}_${inputMethodSubtype.locale}${fileKind.extension()}" val outputFileName = "${fileKind.kind.name.lowercase()}_${inputMethodSubtype.locale}${fileKind.kind.extension()}"
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// This is a special case for now // This is a special case for now
if (fileKind == FileKind.Transformer) { if (fileKind.kind == FileKind.Transformer) {
// 1. Copy file // 1. Copy file
val contentResolver = applicationContext.contentResolver val contentResolver = applicationContext.contentResolver
val outDirectory = ModelPaths.getModelDirectory(applicationContext) val outDirectory = ModelPaths.getModelDirectory(applicationContext)
@ -322,8 +392,16 @@ class ImportResourceActivity : ComponentActivity() {
} }
// 2. Update reference // 2. Update reference
val key = fileKind.preferencesKeyFor(inputMethodSubtype.locale) applicationContext.setSetting(
applicationContext.setSetting(key, outputFileName) fileKind.kind.preferenceKeyFor(inputMethodSubtype.locale),
outputFileName
)
fileKind.name?.let {
applicationContext.setSetting(
fileKind.kind.namePreferenceKeyFor(inputMethodSubtype.locale),
it
)
}
} }
} }
LatinIMELegacy.mPendingDictionaryUpdate = true LatinIMELegacy.mPendingDictionaryUpdate = true

View File

@ -20,6 +20,8 @@ import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.RichInputMethodManager import org.futo.inputmethod.latin.RichInputMethodManager
import org.futo.inputmethod.latin.uix.FileKind import org.futo.inputmethod.latin.uix.FileKind
import org.futo.inputmethod.latin.uix.ResourceHelper import org.futo.inputmethod.latin.uix.ResourceHelper
import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.latin.uix.namePreferenceKeyFor
import org.futo.inputmethod.latin.uix.settings.NavigationItem import org.futo.inputmethod.latin.uix.settings.NavigationItem
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
import org.futo.inputmethod.latin.uix.settings.ScreenTitle import org.futo.inputmethod.latin.uix.settings.ScreenTitle
@ -118,8 +120,8 @@ fun LanguagesScreen(navController: NavHostController = rememberNavController())
val locale = Locale.forLanguageTag(it.locale.replace("_", "-")) val locale = Locale.forLanguageTag(it.locale.replace("_", "-"))
val voiceInputModelName = ResourceHelper.tryFindingVoiceInputModelForLocale(context, locale)?.name?.let { stringResource(it) } val voiceInputModelName = ResourceHelper.tryFindingVoiceInputModelForLocale(context, locale)?.name?.let { stringResource(it) }
val dictionaryName = runBlocking { ResourceHelper.findFileForKind(context, locale, FileKind.Dictionary) }?.let { val dictionaryName = runBlocking { ResourceHelper.findKeyForLocaleAndKind(context, locale, FileKind.Dictionary) }?.let {
"Imported Dictionary" runBlocking { context.getSetting(FileKind.Dictionary.namePreferenceKeyFor(it), "Dictionary") } + " (Imported)"
} ?: if(BinaryDictionaryGetter.getDictionaryFiles(locale, context, false, false).isNotEmpty()) { } ?: if(BinaryDictionaryGetter.getDictionaryFiles(locale, context, false, false).isNotEmpty()) {
"Built-in Dictionary" "Built-in Dictionary"
} else { } else {