mirror of
https://github.com/florisboard/florisboard.git
synced 2024-09-19 11:32:18 +02:00
Add Chinese Shape Based Layouts (#2054)
* feat(ime/nlp): Add `HanShapeBasedLanguageProvider` * feat: Manually set default NLP to be HanShapeBased * feat: Temporarily disable adding spaces This commit should give insight into how the keyboard adds spaces, this should then be refined into not adding a space after commiting a CJK text suggestion * fix(ime/nlp): Remove empty str suggest in HanShape * feat(ime/nlp): Handle locale variants in HanShape this should facilitate multiple layouts in the zh locale * fix(ime/nlp): Handle query params in HanShape This also helps performance as the DBC doesn't have to compile the query for every string the user writes * Space behavior QoL updates for Han shape-based layout (#1) * Separate space behavior for zh* and latin, and allow space when there is no suggestion. Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Add checking if locale is CJK Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * refactor: Change predicate to a getter & rename * chore: Remove TODO `supportsAutoSpace` message * fix: Fix spaces after sugg. in non-space subtypes * fix: Fix auto space predicate in `PhantomSpace` Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> Co-authored-by: waelwindows <waelwindows9922@gmail.com> * Draft: editor screen exposes nlpProviders and shape-based Chinese input methods as variants Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Fix defaults for zhengma preset Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Add word tables for added input methods Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Fix: bug in zhengma preset * Draft: support composing with special characters by delegating nlpProvider to decide composing range. * Catch SQLite errors such as layout (locale variant) not found (e.g. using HanShapeBased with JIS) * fixup: remove TODO * fix: partly addresses 2101, allow searching for locale in English for phones lacking system locale IME * Adds support for importing "language packs" (sqlite3 db for HanShapeBased for now) * Changes language pack to zip files. Adds a basic language pack class for storing metadata of IMEs. Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Implement language pack as a type of Flex extension, and draft its import and view UI Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * fix: input method name translation Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Trim down to zhengma, quickclassic, and cangjie for the barebones Chinese shape-based pack. Polish extension user documentation. * Fix hack to allow multiple language pack extensions to co-exist. Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> * Replace quickclassic with boshiamy * Fix href in LANGUAGEPACKS.md * build(nix): Clean up nix flake * refactor: Encapsulate lanaguage pack query in HSB * feat(ime/nlp): Implement `getListOfWords` in HSB * feat(ime/nlp): Implement `getFrequencyForWord` * chore: Normalize weights for freq in `han.sqlite3` * chore(ime/nlp): Add some logging for HSB * Update app/src/main/assets/ime/keyboard/org.florisboard.localization/extension.json Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev> Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me> Co-authored-by: moonbeamcelery <114041522+moonbeamcelery@users.noreply.github.com> Co-authored-by: moonbeamcelery <moonbeamcelery@proton.me> Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
This commit is contained in:
parent
9776ac1812
commit
a5dab5fb5a
4
.gitignore
vendored
4
.gitignore
vendored
@ -43,3 +43,7 @@ crowdin.properties
|
|||||||
|
|
||||||
# C++
|
# C++
|
||||||
.cxx/
|
.cxx/
|
||||||
|
|
||||||
|
# Nix stuff
|
||||||
|
.direnv/
|
||||||
|
result
|
||||||
|
67
LANGUAGEPACKS.md
Normal file
67
LANGUAGEPACKS.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Language Packs
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
- [Summary](#summary)
|
||||||
|
- [Chinese / 中文](#chinese--中文)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Stub.
|
||||||
|
|
||||||
|
This page should describe how language packs work, how to import them, and point to the location of downloadable
|
||||||
|
language packs.
|
||||||
|
|
||||||
|
The homepage of default language packs included in FlorisBoard should link to this page.
|
||||||
|
|
||||||
|
## Chinese / 中文
|
||||||
|
|
||||||
|
TODO: translate into native language
|
||||||
|
|
||||||
|
### Default barebones Chinese shape-based pack
|
||||||
|
|
||||||
|
默认中文形码语言包 / 預設中文形碼語言包 / Default barebones Chinese shape-based language pack which are always available.
|
||||||
|
|
||||||
|
Please download the [full Chinese language pack](#full-chinese-shape-based-pack) to access input methods
|
||||||
|
such as wubi, boshiami, and other versions of cangjie.
|
||||||
|
|
||||||
|
This pack is released under the same license as Florisboard.
|
||||||
|
|
||||||
|
TODO: translate into native language
|
||||||
|
|
||||||
|
### Full Chinese shape-based pack
|
||||||
|
|
||||||
|
Fcitx5 中文形码语言包 / Fcitx5 中文形碼語言包 / Chinese shape-based language pack based on fcitx5-table-extra.
|
||||||
|
|
||||||
|
This pack is released under a separate license. Please visit: [TODO: add link to release page](https://)
|
||||||
|
|
||||||
|
The following input methods are included in this language pack:
|
||||||
|
|
||||||
|
- 中文 (中国) [T9笔画] / Chinese (China) [T9]
|
||||||
|
- 中文 (中国) [五笔98] / Chinese (China) [WUBI98]
|
||||||
|
- 中文 (中国) [五笔98-拼音混打] / Chinese (China) [WUBI98PINYIN]
|
||||||
|
- 中文 (中国) [五笔98-单字] / Chinese (China) [WUBI98SINGLE]
|
||||||
|
- 中文 (中国) [五笔-大字库] / Chinese (China) [WUBILARGE]
|
||||||
|
- 中文 (中国) [郑码] / Chinese (China) [ZHENGMA]
|
||||||
|
- 中文 (中国) [郑码-大字库] / Chinese (China) [ZHENGMALARGE]
|
||||||
|
- 中文 (中国) [郑码-拼音混打] / Chinese (China) [ZHENGMAPINYIN]
|
||||||
|
- 中文 (香港) [廣東拼音] / Chinese (Hong Kong) [CANTONESE]
|
||||||
|
- 中文 (香港) [港式廣東話] / Chinese (Hong Kong) [CANTONHK]
|
||||||
|
- 中文 (香港) [輕鬆-大字庫] / Chinese (Hong Kong) [EASYLARGE]
|
||||||
|
- 中文 (香港) [粤語拼音-表格] / Chinese (Hong Kong) [JYUTPINGTABLE]
|
||||||
|
- 中文 (香港) [速成三代] / Chinese (Hong Kong) [QUICK3]
|
||||||
|
- 中文 (香港) [經典速成] / Chinese (Hong Kong) [QUICKCLASSIC]
|
||||||
|
- 中文 (香港) [筆順五碼] / Chinese (Hong Kong) [STROKE5]
|
||||||
|
- 中文 (台灣) [行列] / Chinese (Taiwan) [ARRAY30]
|
||||||
|
- 中文 (台灣) [行列-大字库] / Chinese (Taiwan) [ARRAY30LARGE]
|
||||||
|
- 中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]
|
||||||
|
- 中文 (台灣) [倉頡三代] / Chinese (Taiwan) [CANGJIE3]
|
||||||
|
- 中文 (台灣) [倉頡五代] / Chinese (Taiwan) [CANGJIE5]
|
||||||
|
- 中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]
|
||||||
|
- 中文 (台灣) [速成五代] / Chinese (Taiwan) [QUICK5]
|
||||||
|
- 中文 (台灣) [快倉六] / Chinese (Taiwan) [SCJ6]
|
||||||
|
- 中文 (台灣) [吳語注音] / Chinese (Taiwan) [WU]
|
||||||
|
|
||||||
|
Third-party license: [https://github.com/fcitx/fcitx5-table-extra/blob/master/LICENSES/GPL-3.0-or-later.txt]
|
||||||
|
|
||||||
|
TODO: translate into native language
|
@ -646,6 +646,21 @@
|
|||||||
"preferred": {
|
"preferred": {
|
||||||
"characters": "org.florisboard.layouts:urdu_phonetic"
|
"characters": "org.florisboard.layouts:urdu_phonetic"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"languageTag": "zh-CN-zhengma",
|
||||||
|
"composer": "org.florisboard.composers:appender",
|
||||||
|
"nlpProviders": {
|
||||||
|
"spelling": "org.florisboard.nlp.providers.han.shape",
|
||||||
|
"suggestion": "org.florisboard.nlp.providers.han.shape"
|
||||||
|
},
|
||||||
|
"currencySet": "org.florisboard.currencysets:yen",
|
||||||
|
"popupMapping": "org.florisboard.localization:en",
|
||||||
|
"preferred": {
|
||||||
|
"characters": "org.florisboard.layouts:qwerty",
|
||||||
|
"symbols": "org.florisboard.layouts:cjk",
|
||||||
|
"symbols2": "org.florisboard.layouts:cjk"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"languageTag": "bn-BD",
|
"languageTag": "bn-BD",
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$": "ime.extension.languagepack",
|
||||||
|
"meta": {
|
||||||
|
"id": "org.florisboard.hanshapebasedbasicpack",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"title": "Default barebones Chinese shape-based pack",
|
||||||
|
"description": "默认中文形码语言包 / 預設中文形碼語言包 / Default barebones Chinese shape-based language pack which are always available.\n请访问下面的主页链接下载更多输入法 / 請訪問下面的主頁鏈接下載更多輸入法 / Please visit the homepage linked below to download more input methods.",
|
||||||
|
"maintainers": [ "patrickgold <patrick@patrickgold.dev>", "waelwindows", "moonbeamcelery" ],
|
||||||
|
"homepage": "https://github.com/florisboard/florisboard/blob/master/LANGUAGEPACKS.md#default-barebones-chinese-shape-based-pack",
|
||||||
|
"license": "apache-2.0"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "zh_CN_zhengma",
|
||||||
|
"label": "中文 (中国) [郑码] / Chinese (China) [ZHENGMA]",
|
||||||
|
"authors": [ "waelwindows", "moonbeamcelery" ],
|
||||||
|
"hanShapeBasedKeyCode": "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zh_TW_boshiamy",
|
||||||
|
"label": "中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]",
|
||||||
|
"authors": [ "waelwindows", "moonbeamcelery" ],
|
||||||
|
"hanShapeBasedKeyCode": ",.'abcdefghijklmnopqrstuvwxyz[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zh_TW_cangjielarge",
|
||||||
|
"label": "中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]",
|
||||||
|
"authors": [ "waelwindows", "moonbeamcelery" ],
|
||||||
|
"hanShapeBasedKeyCode": "abcdefghijklmnopqrstuvwxyz&"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Binary file not shown.
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$": "ime.extension.languagepack",
|
||||||
|
"meta": {
|
||||||
|
"id": "org.florisboard.languagepack",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"title": "Default language pack",
|
||||||
|
"description": "Default language pack which are always available.",
|
||||||
|
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
|
||||||
|
"license": "apache-2.0"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "de_DE_neobone",
|
||||||
|
"label": "Deutsch (Deutschland) [NEOBONE] / German (Germany) [NEOBONE]",
|
||||||
|
"authors": [ "patrickgold" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ja_JP_jis",
|
||||||
|
"label": "日本語 (日本) [JIS] / Japanese (Japan) [JIS]",
|
||||||
|
"authors": [ "patrickgold" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -44,6 +44,8 @@ import dev.patrickgold.florisboard.app.settings.dictionary.UserDictionaryType
|
|||||||
import dev.patrickgold.florisboard.app.settings.gestures.GesturesScreen
|
import dev.patrickgold.florisboard.app.settings.gestures.GesturesScreen
|
||||||
import dev.patrickgold.florisboard.app.settings.keyboard.InputFeedbackScreen
|
import dev.patrickgold.florisboard.app.settings.keyboard.InputFeedbackScreen
|
||||||
import dev.patrickgold.florisboard.app.settings.keyboard.KeyboardScreen
|
import dev.patrickgold.florisboard.app.settings.keyboard.KeyboardScreen
|
||||||
|
import dev.patrickgold.florisboard.app.settings.localization.LanguagePackManagerScreen
|
||||||
|
import dev.patrickgold.florisboard.app.settings.localization.LanguagePackManagerScreenAction
|
||||||
import dev.patrickgold.florisboard.app.settings.localization.LocalizationScreen
|
import dev.patrickgold.florisboard.app.settings.localization.LocalizationScreen
|
||||||
import dev.patrickgold.florisboard.app.settings.localization.SelectLocaleScreen
|
import dev.patrickgold.florisboard.app.settings.localization.SelectLocaleScreen
|
||||||
import dev.patrickgold.florisboard.app.settings.localization.SubtypeEditorScreen
|
import dev.patrickgold.florisboard.app.settings.localization.SubtypeEditorScreen
|
||||||
@ -67,6 +69,9 @@ object Routes {
|
|||||||
|
|
||||||
const val Localization = "settings/localization"
|
const val Localization = "settings/localization"
|
||||||
const val SelectLocale = "settings/localization/select-locale"
|
const val SelectLocale = "settings/localization/select-locale"
|
||||||
|
const val LanguagePackManager = "settings/localization/language-pack-manage/{action}"
|
||||||
|
fun LanguagePackManager(action: LanguagePackManagerScreenAction) =
|
||||||
|
LanguagePackManager.curlyFormat("action" to action.id)
|
||||||
const val SubtypeAdd = "settings/localization/subtype/add"
|
const val SubtypeAdd = "settings/localization/subtype/add"
|
||||||
const val SubtypeEdit = "settings/localization/subtype/edit/{id}"
|
const val SubtypeEdit = "settings/localization/subtype/edit/{id}"
|
||||||
fun SubtypeEdit(id: Long) = SubtypeEdit.curlyFormat("id" to id)
|
fun SubtypeEdit(id: Long) = SubtypeEdit.curlyFormat("id" to id)
|
||||||
@ -147,6 +152,12 @@ object Routes {
|
|||||||
|
|
||||||
composable(Settings.Localization) { LocalizationScreen() }
|
composable(Settings.Localization) { LocalizationScreen() }
|
||||||
composable(Settings.SelectLocale) { SelectLocaleScreen() }
|
composable(Settings.SelectLocale) { SelectLocaleScreen() }
|
||||||
|
composable(Settings.LanguagePackManager) { navBackStack ->
|
||||||
|
val action = navBackStack.arguments?.getString("action")?.let { actionId ->
|
||||||
|
LanguagePackManagerScreenAction.values().firstOrNull { it.id == actionId }
|
||||||
|
}
|
||||||
|
LanguagePackManagerScreen(action)
|
||||||
|
}
|
||||||
composable(Settings.SubtypeAdd) { SubtypeEditorScreen(null) }
|
composable(Settings.SubtypeAdd) { SubtypeEditorScreen(null) }
|
||||||
composable(Settings.SubtypeEdit) { navBackStack ->
|
composable(Settings.SubtypeEdit) { navBackStack ->
|
||||||
val id = navBackStack.arguments?.getString("id")?.toLongOrNull()
|
val id = navBackStack.arguments?.getString("id")?.toLongOrNull()
|
||||||
|
@ -37,6 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackComponent
|
||||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
|
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
||||||
@ -96,6 +97,22 @@ fun ExtensionComponentView(
|
|||||||
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is LanguagePackComponent -> {
|
||||||
|
val text = remember(
|
||||||
|
component.authors, component.locale, component.hanShapeBasedKeyCode,
|
||||||
|
) {
|
||||||
|
buildString {
|
||||||
|
appendLine("authors = ${component.authors}")
|
||||||
|
appendLine("locale = ${component.locale.localeTag()}")
|
||||||
|
appendLine("hanShapeBasedKeyCode = ${component.hanShapeBasedKeyCode}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ import dev.patrickgold.florisboard.app.LocalNavController
|
|||||||
import dev.patrickgold.florisboard.cacheManager
|
import dev.patrickgold.florisboard.cacheManager
|
||||||
import dev.patrickgold.florisboard.extensionManager
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||||
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
|
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
|
||||||
import dev.patrickgold.florisboard.lib.android.showLongToast
|
import dev.patrickgold.florisboard.lib.android.showLongToast
|
||||||
@ -60,6 +61,7 @@ import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
|||||||
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||||
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
|
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
|
||||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.florisboard.lib.devtools.flogDebug
|
||||||
import dev.patrickgold.florisboard.lib.io.FileRegistry
|
import dev.patrickgold.florisboard.lib.io.FileRegistry
|
||||||
import dev.patrickgold.florisboard.lib.kotlin.resultOk
|
import dev.patrickgold.florisboard.lib.kotlin.resultOk
|
||||||
|
|
||||||
@ -82,6 +84,11 @@ enum class ExtensionImportScreenType(
|
|||||||
id = "ext-theme",
|
id = "ext-theme",
|
||||||
titleResId = R.string.ext__import__ext_theme,
|
titleResId = R.string.ext__import__ext_theme,
|
||||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||||
|
),
|
||||||
|
EXT_LANGUAGEPACK(
|
||||||
|
id = "ext-languagepack",
|
||||||
|
titleResId = R.string.ext__import__ext_languagepack,
|
||||||
|
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +181,9 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
|||||||
ExtensionImportScreenType.EXT_THEME -> {
|
ExtensionImportScreenType.EXT_THEME -> {
|
||||||
ext.takeIf { it is ThemeExtension }?.let { extensionManager.import(it) }
|
ext.takeIf { it is ThemeExtension }?.let { extensionManager.import(it) }
|
||||||
}
|
}
|
||||||
|
ExtensionImportScreenType.EXT_LANGUAGEPACK -> {
|
||||||
|
ext.takeIf { it is LanguagePackExtension }?.let { extensionManager.import(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
|
@ -46,6 +46,7 @@ import dev.patrickgold.florisboard.R
|
|||||||
import dev.patrickgold.florisboard.app.LocalNavController
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
import dev.patrickgold.florisboard.app.Routes
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
import dev.patrickgold.florisboard.extensionManager
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentImpl
|
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentImpl
|
||||||
import dev.patrickgold.florisboard.lib.android.showLongToast
|
import dev.patrickgold.florisboard.lib.android.showLongToast
|
||||||
@ -174,6 +175,18 @@ private fun ViewScreen(ext: Extension) = FlorisScreen {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is LanguagePackExtension -> {
|
||||||
|
ExtensionComponentListView(
|
||||||
|
title = stringRes(R.string.ext__meta__components_language_pack),
|
||||||
|
components = ext.items,
|
||||||
|
) { component ->
|
||||||
|
ExtensionComponentView(
|
||||||
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
|
meta = ext.meta,
|
||||||
|
component = component,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Render nothing
|
// Render nothing
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Patrick Goldinger
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.patrickgold.florisboard.app.settings.localization
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.RadioButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.patrickgold.florisboard.R
|
||||||
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
|
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
|
||||||
|
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||||
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackComponent
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
|
import dev.patrickgold.florisboard.lib.android.showLongToast
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisConfirmDeleteDialog
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.rippleClickable
|
||||||
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.Extension
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
|
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||||
|
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||||
|
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
||||||
|
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||||
|
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
|
||||||
|
|
||||||
|
enum class LanguagePackManagerScreenAction(val id: String) {
|
||||||
|
MANAGE("manage-installed-language-packs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this file is based on ThemeManagerScreen.kt and can arguably be merged.
|
||||||
|
@OptIn(ExperimentalJetPrefDatastoreUi::class)
|
||||||
|
@Composable
|
||||||
|
fun LanguagePackManagerScreen(action: LanguagePackManagerScreenAction?) = FlorisScreen {
|
||||||
|
title = stringRes(when (action) {
|
||||||
|
LanguagePackManagerScreenAction.MANAGE -> R.string.settings__localization__language_pack_title
|
||||||
|
else -> error("LanguagePack manager screen action must not be null")
|
||||||
|
})
|
||||||
|
|
||||||
|
val prefs by florisPreferenceModel()
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val extensionManager by context.extensionManager()
|
||||||
|
|
||||||
|
val indexedLanguagePackExtensions by extensionManager.languagePacks.observeAsNonNullState()
|
||||||
|
val selectedManagerLanguagePackId = remember { mutableStateOf<ExtensionComponentName?>(null) }
|
||||||
|
val extGroupedLanguagePacks = remember(indexedLanguagePackExtensions) {
|
||||||
|
buildMap<String, List<LanguagePackComponent>> {
|
||||||
|
for (ext in indexedLanguagePackExtensions) {
|
||||||
|
put(ext.meta.id, ext.items)
|
||||||
|
}
|
||||||
|
}.mapValues { (_, configs) -> configs.sortedBy { it.label } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLanguagePackIdPref(): Nothing = TODO("Not implemented yet")
|
||||||
|
|
||||||
|
fun setLanguagePack(extId: String, componentId: String) {
|
||||||
|
val extComponentName = ExtensionComponentName(extId, componentId)
|
||||||
|
when (action) {
|
||||||
|
LanguagePackManagerScreenAction.MANAGE -> {
|
||||||
|
selectedManagerLanguagePackId.value = extComponentName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeLanguagePackId by when (action) {
|
||||||
|
LanguagePackManagerScreenAction.MANAGE -> selectedManagerLanguagePackId
|
||||||
|
}
|
||||||
|
var languagePackExtToDelete by remember { mutableStateOf<Extension?>(null) }
|
||||||
|
|
||||||
|
content {
|
||||||
|
val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
|
||||||
|
if (action == LanguagePackManagerScreenAction.MANAGE) {
|
||||||
|
FlorisOutlinedBox(
|
||||||
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
|
) {
|
||||||
|
this@content.Preference(
|
||||||
|
onClick = { navController.navigate(
|
||||||
|
Routes.Ext.Import(ExtensionImportScreenType.EXT_LANGUAGEPACK, null)
|
||||||
|
) },
|
||||||
|
iconId = R.drawable.ic_input,
|
||||||
|
title = stringRes(R.string.action__import),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ((extensionId, configs) in extGroupedLanguagePacks) key(extensionId) {
|
||||||
|
val ext = extensionManager.getExtensionById(extensionId)!!
|
||||||
|
FlorisOutlinedBox(
|
||||||
|
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||||
|
title = ext.meta.title,
|
||||||
|
onTitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
||||||
|
subtitle = extensionId,
|
||||||
|
onSubtitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
// Allowing horizontal scroll to fit translations in descriptions.
|
||||||
|
Modifier.horizontalScroll(rememberScrollState()).width(intrinsicSize = IntrinsicSize.Max),
|
||||||
|
) {
|
||||||
|
for (config in configs) key(extensionId, config.id) {
|
||||||
|
JetPrefListItem(
|
||||||
|
modifier = Modifier.rippleClickable {
|
||||||
|
setLanguagePack(extensionId, config.id)
|
||||||
|
},
|
||||||
|
// icon = {
|
||||||
|
// RadioButton(
|
||||||
|
// selected = activeLanguagePackId?.extensionId == extensionId &&
|
||||||
|
// activeLanguagePackId?.componentId == config.id,
|
||||||
|
// onClick = null,
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
text = config.label,
|
||||||
|
// trailing = {
|
||||||
|
// Icon(
|
||||||
|
// modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||||
|
// painter = painterResource(if (config.isNightLanguagePack) {
|
||||||
|
// R.drawable.ic_dark_mode
|
||||||
|
// } else {
|
||||||
|
// R.drawable.ic_light_mode
|
||||||
|
// }),
|
||||||
|
// contentDescription = null,
|
||||||
|
// tint = grayColor,
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action == LanguagePackManagerScreenAction.MANAGE && extensionManager.canDelete(ext)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
|
) {
|
||||||
|
FlorisTextButton(
|
||||||
|
onClick = {
|
||||||
|
languagePackExtToDelete = ext
|
||||||
|
},
|
||||||
|
icon = painterResource(R.drawable.ic_delete),
|
||||||
|
text = stringRes(R.string.action__delete),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = MaterialTheme.colors.error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
// FlorisTextButton(
|
||||||
|
// onClick = {
|
||||||
|
// navController.navigate(Routes.Ext.Edit(ext.meta.id))
|
||||||
|
// },
|
||||||
|
// icon = painterResource(R.drawable.ic_edit),
|
||||||
|
// text = stringRes(R.string.action__edit),
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languagePackExtToDelete != null) {
|
||||||
|
FlorisConfirmDeleteDialog(
|
||||||
|
onConfirm = {
|
||||||
|
runCatching {
|
||||||
|
extensionManager.delete(languagePackExtToDelete!!)
|
||||||
|
}.onFailure { error ->
|
||||||
|
context.showLongToast(
|
||||||
|
R.string.error__snackbar_message,
|
||||||
|
"error_message" to error.localizedMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
languagePackExtToDelete = null
|
||||||
|
},
|
||||||
|
onDismiss = { languagePackExtToDelete = null },
|
||||||
|
what = languagePackExtToDelete!!.meta.title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.app.settings.localization
|
package dev.patrickgold.florisboard.app.settings.localization
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.ExtendedFloatingActionButton
|
import androidx.compose.material.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
@ -30,12 +32,22 @@ import androidx.compose.ui.unit.dp
|
|||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.app.LocalNavController
|
import dev.patrickgold.florisboard.app.LocalNavController
|
||||||
import dev.patrickgold.florisboard.app.Routes
|
import dev.patrickgold.florisboard.app.Routes
|
||||||
|
import dev.patrickgold.florisboard.app.settings.advanced.Restore
|
||||||
|
import dev.patrickgold.florisboard.app.settings.theme.ThemeManagerScreenAction
|
||||||
|
import dev.patrickgold.florisboard.cacheManager
|
||||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
|
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
|
||||||
import dev.patrickgold.florisboard.keyboardManager
|
import dev.patrickgold.florisboard.keyboardManager
|
||||||
|
import dev.patrickgold.florisboard.lib.android.readToFile
|
||||||
|
import dev.patrickgold.florisboard.lib.android.showLongToast
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisWarningCard
|
import dev.patrickgold.florisboard.lib.compose.FlorisWarningCard
|
||||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||||
|
import dev.patrickgold.florisboard.lib.io.ZipUtils
|
||||||
|
import dev.patrickgold.florisboard.lib.io.parentDir
|
||||||
|
import dev.patrickgold.florisboard.lib.io.subFile
|
||||||
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||||
import dev.patrickgold.florisboard.subtypeManager
|
import dev.patrickgold.florisboard.subtypeManager
|
||||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||||
@ -53,6 +65,7 @@ fun LocalizationScreen() = FlorisScreen {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val keyboardManager by context.keyboardManager()
|
val keyboardManager by context.keyboardManager()
|
||||||
val subtypeManager by context.subtypeManager()
|
val subtypeManager by context.subtypeManager()
|
||||||
|
val cacheManager by context.cacheManager()
|
||||||
|
|
||||||
floatingActionButton {
|
floatingActionButton {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
@ -67,12 +80,21 @@ fun LocalizationScreen() = FlorisScreen {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
content {
|
content {
|
||||||
ListPreference(
|
ListPreference(
|
||||||
prefs.localization.displayLanguageNamesIn,
|
prefs.localization.displayLanguageNamesIn,
|
||||||
title = stringRes(R.string.settings__localization__display_language_names_in__label),
|
title = stringRes(R.string.settings__localization__display_language_names_in__label),
|
||||||
entries = DisplayLanguageNamesIn.listEntries(),
|
entries = DisplayLanguageNamesIn.listEntries(),
|
||||||
)
|
)
|
||||||
|
Preference(
|
||||||
|
// iconId = R.drawable.ic_edit,
|
||||||
|
title = stringRes(R.string.settings__localization__language_pack_title),
|
||||||
|
summary = stringRes(R.string.settings__localization__language_pack_summary),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(Routes.Settings.LanguagePackManager(LanguagePackManagerScreenAction.MANAGE))
|
||||||
|
},
|
||||||
|
)
|
||||||
PreferenceGroup(title = stringRes(R.string.settings__localization__group_subtypes__label)) {
|
PreferenceGroup(title = stringRes(R.string.settings__localization__group_subtypes__label)) {
|
||||||
val subtypes by subtypeManager.subtypesFlow.collectAsState()
|
val subtypes by subtypeManager.subtypesFlow.collectAsState()
|
||||||
if (subtypes.isEmpty()) {
|
if (subtypes.isEmpty()) {
|
||||||
|
@ -38,6 +38,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -64,14 +65,15 @@ fun SelectLocaleScreen() = FlorisScreen {
|
|||||||
|
|
||||||
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
|
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
|
||||||
var searchTermValue by remember { mutableStateOf(TextFieldValue()) }
|
var searchTermValue by remember { mutableStateOf(TextFieldValue()) }
|
||||||
val systemLocales = remember(displayLanguageNamesIn) {
|
val context = LocalContext.current
|
||||||
FlorisLocale.installedSystemLocales().sortedBy { locale ->
|
val systemLocales =
|
||||||
|
FlorisLocale.extendedAvailableLocales(context).sortedBy { locale ->
|
||||||
when (displayLanguageNamesIn) {
|
when (displayLanguageNamesIn) {
|
||||||
DisplayLanguageNamesIn.SYSTEM_LOCALE -> locale.displayName()
|
DisplayLanguageNamesIn.SYSTEM_LOCALE -> locale.displayName()
|
||||||
DisplayLanguageNamesIn.NATIVE_LOCALE -> locale.displayName(locale)
|
DisplayLanguageNamesIn.NATIVE_LOCALE -> locale.displayName(locale)
|
||||||
}.lowercase()
|
}.lowercase()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val filteredSystemLocales = remember(searchTermValue) {
|
val filteredSystemLocales = remember(searchTermValue) {
|
||||||
if (searchTermValue.text.isBlank()) {
|
if (searchTermValue.text.isBlank()) {
|
||||||
systemLocales
|
systemLocales
|
||||||
@ -80,6 +82,7 @@ fun SelectLocaleScreen() = FlorisScreen {
|
|||||||
systemLocales.filter { locale ->
|
systemLocales.filter { locale ->
|
||||||
locale.displayName().lowercase().contains(term) ||
|
locale.displayName().lowercase().contains(term) ||
|
||||||
locale.displayName(locale).lowercase().contains(term) ||
|
locale.displayName(locale).lowercase().contains(term) ||
|
||||||
|
locale.displayName(FlorisLocale.ENGLISH).lowercase().contains(term) ||
|
||||||
locale.languageTag().lowercase().startsWith(term) ||
|
locale.languageTag().lowercase().startsWith(term) ||
|
||||||
locale.localeTag().lowercase().startsWith(term)
|
locale.localeTag().lowercase().startsWith(term)
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,9 @@ import dev.patrickgold.florisboard.ime.core.SubtypePreset
|
|||||||
import dev.patrickgold.florisboard.ime.keyboard.LayoutArrangementComponent
|
import dev.patrickgold.florisboard.ime.keyboard.LayoutArrangementComponent
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
|
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
|
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
|
||||||
|
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
|
||||||
import dev.patrickgold.florisboard.keyboardManager
|
import dev.patrickgold.florisboard.keyboardManager
|
||||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||||
import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
|
import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
|
||||||
@ -136,6 +139,7 @@ private class SubtypeEditorState(init: Subtype?) {
|
|||||||
primaryLocale.value = subtype.primaryLocale
|
primaryLocale.value = subtype.primaryLocale
|
||||||
secondaryLocales.value = subtype.secondaryLocales
|
secondaryLocales.value = subtype.secondaryLocales
|
||||||
composer.value = subtype.composer
|
composer.value = subtype.composer
|
||||||
|
nlpProviders.value = subtype.nlpProviders
|
||||||
currencySet.value = subtype.currencySet
|
currencySet.value = subtype.currencySet
|
||||||
punctuationRule.value = subtype.punctuationRule
|
punctuationRule.value = subtype.punctuationRule
|
||||||
popupMapping.value = subtype.popupMapping
|
popupMapping.value = subtype.popupMapping
|
||||||
@ -201,6 +205,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
|
|||||||
var currencySet by subtypeEditor.currencySet
|
var currencySet by subtypeEditor.currencySet
|
||||||
var popupMapping by subtypeEditor.popupMapping
|
var popupMapping by subtypeEditor.popupMapping
|
||||||
var layoutMap by subtypeEditor.layoutMap
|
var layoutMap by subtypeEditor.layoutMap
|
||||||
|
var nlpProviders by subtypeEditor.nlpProviders
|
||||||
|
|
||||||
var showSubtypePresetsDialog by rememberSaveable { mutableStateOf(false) }
|
var showSubtypePresetsDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
|
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
|
||||||
@ -371,6 +376,38 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
|
|||||||
|
|
||||||
SubtypeGroupSpacer()
|
SubtypeGroupSpacer()
|
||||||
|
|
||||||
|
SubtypeProperty(stringRes(R.string.settings__localization__subtype_suggestion_provider)) {
|
||||||
|
// TODO: Put this map somewhere more formal (another KeyboardExtension field?)
|
||||||
|
// optionally use a string resource below
|
||||||
|
val nlpProviderMappings = mapOf(
|
||||||
|
LatinLanguageProvider.ProviderId to "Latin",
|
||||||
|
HanShapeBasedLanguageProvider.ProviderId to "Chinese shape-based"
|
||||||
|
)
|
||||||
|
|
||||||
|
val nlpProviderMappingIds = remember(nlpProviderMappings) {
|
||||||
|
SelectListKeys + nlpProviderMappings.keys
|
||||||
|
}
|
||||||
|
val nlpProviderMappingLabels = remember(nlpProviderMappings) {
|
||||||
|
selectListValues + nlpProviderMappings.values.map { it }
|
||||||
|
}
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val selectedIndex = nlpProviderMappingIds.indexOf(nlpProviders.suggestion).coerceAtLeast(0)
|
||||||
|
FlorisDropdownMenu(
|
||||||
|
items = nlpProviderMappingLabels,
|
||||||
|
expanded = expanded,
|
||||||
|
selectedIndex = selectedIndex,
|
||||||
|
isError = showSelectAsError && selectedIndex == 0,
|
||||||
|
onSelectItem = { nlpProviders = SubtypeNlpProviderMap(
|
||||||
|
suggestion = nlpProviderMappingIds[it] as String,
|
||||||
|
spelling = nlpProviderMappingIds[it] as String
|
||||||
|
) },
|
||||||
|
onExpandRequest = { expanded = true },
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SubtypeGroupSpacer()
|
||||||
|
|
||||||
SubtypeProperty(stringRes(R.string.settings__localization__subtype_symbols_layout)) {
|
SubtypeProperty(stringRes(R.string.settings__localization__subtype_symbols_layout)) {
|
||||||
val layoutType = LayoutType.SYMBOLS
|
val layoutType = LayoutType.SYMBOLS
|
||||||
SubtypeLayoutDropdown(
|
SubtypeLayoutDropdown(
|
||||||
|
@ -24,6 +24,7 @@ import dev.patrickgold.florisboard.ime.keyboard.extCoreLayout
|
|||||||
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
|
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.extCorePunctuationRule
|
import dev.patrickgold.florisboard.ime.keyboard.extCorePunctuationRule
|
||||||
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
|
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
|
||||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
|
@ -31,6 +31,7 @@ import dev.patrickgold.florisboard.ime.text.composing.Composer
|
|||||||
import dev.patrickgold.florisboard.keyboardManager
|
import dev.patrickgold.florisboard.keyboardManager
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
import dev.patrickgold.florisboard.lib.kotlin.guardedByLock
|
import dev.patrickgold.florisboard.lib.kotlin.guardedByLock
|
||||||
|
import dev.patrickgold.florisboard.nlpManager
|
||||||
import dev.patrickgold.florisboard.subtypeManager
|
import dev.patrickgold.florisboard.subtypeManager
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -53,6 +54,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
|||||||
|
|
||||||
private val keyboardManager by context.keyboardManager()
|
private val keyboardManager by context.keyboardManager()
|
||||||
private val subtypeManager by context.subtypeManager()
|
private val subtypeManager by context.subtypeManager()
|
||||||
|
private val nlpManager by context.nlpManager()
|
||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
protected val breakIterators = BreakIteratorGroup()
|
protected val breakIterators = BreakIteratorGroup()
|
||||||
|
|
||||||
@ -274,17 +276,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun determineLocalComposing(textBeforeSelection: CharSequence): EditorRange {
|
private suspend fun determineLocalComposing(textBeforeSelection: CharSequence): EditorRange {
|
||||||
return breakIterators.word(subtypeManager.activeSubtype.primaryLocale) {
|
return nlpManager.determineLocalComposing(textBeforeSelection, breakIterators)
|
||||||
it.setText(textBeforeSelection.toString())
|
|
||||||
val end = it.last()
|
|
||||||
val isWord = it.ruleStatus != BreakIterator.WORD_NONE
|
|
||||||
if (isWord) {
|
|
||||||
val start = it.previous()
|
|
||||||
EditorRange(start, end)
|
|
||||||
} else {
|
|
||||||
EditorRange.Unspecified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun InputConnection.setComposingRegion(composing: EditorRange) {
|
private fun InputConnection.setComposingRegion(composing: EditorRange) {
|
||||||
|
@ -42,6 +42,7 @@ import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
|||||||
import dev.patrickgold.florisboard.lib.android.showShortToast
|
import dev.patrickgold.florisboard.lib.android.showShortToast
|
||||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
import dev.patrickgold.florisboard.nlpManager
|
import dev.patrickgold.florisboard.nlpManager
|
||||||
|
import dev.patrickgold.florisboard.subtypeManager
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
@ -54,6 +55,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
|||||||
private val appContext by context.appContext()
|
private val appContext by context.appContext()
|
||||||
private val clipboardManager by context.clipboardManager()
|
private val clipboardManager by context.clipboardManager()
|
||||||
private val keyboardManager by context.keyboardManager()
|
private val keyboardManager by context.keyboardManager()
|
||||||
|
private val subtypeManager by context.subtypeManager()
|
||||||
private val nlpManager by context.nlpManager()
|
private val nlpManager by context.nlpManager()
|
||||||
|
|
||||||
private val activeState get() = keyboardManager.activeState
|
private val activeState get() = keyboardManager.activeState
|
||||||
@ -512,15 +514,16 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun PhantomSpaceState.determine(text: String, forceActive: Boolean = false): Boolean {
|
private fun PhantomSpaceState.determine(text: String, forceActive: Boolean = false): Boolean {
|
||||||
val content = activeContent
|
val content = activeContent
|
||||||
val selection = content.selection
|
val selection = content.selection
|
||||||
if (!(isActive || forceActive) || selection.isNotValid || selection.start <= 0 || text.isEmpty()) return false
|
if (!(isActive || forceActive) || selection.isNotValid || selection.start <= 0 || text.isEmpty()) return false
|
||||||
val textBefore = content.getTextBeforeCursor(1)
|
val textBefore = content.getTextBeforeCursor(1)
|
||||||
val punctuationRule = nlpManager.getActivePunctuationRule()
|
val punctuationRule = nlpManager.getActivePunctuationRule()
|
||||||
return textBefore.isNotEmpty() &&
|
if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace) return false;
|
||||||
(punctuationRule.symbolsPrecedingPhantomSpace.contains(textBefore[textBefore.length - 1]) ||
|
return textBefore.isNotEmpty() &&
|
||||||
textBefore[textBefore.length - 1].isLetterOrDigit()) &&
|
(punctuationRule.symbolsPrecedingPhantomSpace.contains(textBefore[textBefore.length - 1]) ||
|
||||||
(punctuationRule.symbolsFollowingPhantomSpace.contains(text[0]) || text[0].isLetterOrDigit())
|
textBefore[textBefore.length - 1].isLetterOrDigit()) &&
|
||||||
|
(punctuationRule.symbolsFollowingPhantomSpace.contains(text[0]) || text[0].isLetterOrDigit())
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoSpaceState {
|
class AutoSpaceState {
|
||||||
|
@ -502,7 +502,8 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
|
|||||||
* enabled by the user.
|
* enabled by the user.
|
||||||
*/
|
*/
|
||||||
private fun handleSpace(data: KeyData) {
|
private fun handleSpace(data: KeyData) {
|
||||||
nlpManager.getAutoCommitCandidate()?.let { commitCandidate(it) }
|
val candidate = nlpManager.getAutoCommitCandidate()
|
||||||
|
candidate?.let { commitCandidate(it) }
|
||||||
if (prefs.keyboard.spaceBarSwitchesToCharacters.get()) {
|
if (prefs.keyboard.spaceBarSwitchesToCharacters.get()) {
|
||||||
when (activeState.keyboardMode) {
|
when (activeState.keyboardMode) {
|
||||||
KeyboardMode.NUMERIC_ADVANCED,
|
KeyboardMode.NUMERIC_ADVANCED,
|
||||||
@ -523,7 +524,10 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace &&
|
||||||
|
candidate != null) { /* Do nothing */ } else {
|
||||||
|
editorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
package dev.patrickgold.florisboard.ime.nlp
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.database.sqlite.SQLiteException
|
||||||
|
import dev.patrickgold.florisboard.appContext
|
||||||
|
import dev.patrickgold.florisboard.assetManager
|
||||||
|
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||||
|
import dev.patrickgold.florisboard.lib.android.copy
|
||||||
|
import dev.patrickgold.florisboard.lib.devtools.flogDebug
|
||||||
|
import dev.patrickgold.florisboard.lib.devtools.flogError
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.Extension
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponent
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionEditor
|
||||||
|
import dev.patrickgold.florisboard.lib.ext.ExtensionMeta
|
||||||
|
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||||
|
import dev.patrickgold.florisboard.lib.io.FsDir
|
||||||
|
import dev.patrickgold.florisboard.lib.io.parentDir
|
||||||
|
import dev.patrickgold.florisboard.lib.io.subFile
|
||||||
|
import dev.patrickgold.florisboard.lib.kotlin.tryOrNull
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class LanguagePackComponent(
|
||||||
|
override val id: String,
|
||||||
|
override val label: String,
|
||||||
|
override val authors: List<String>,
|
||||||
|
val locale: FlorisLocale = FlorisLocale.fromTag(id),
|
||||||
|
val hanShapeBasedKeyCode: String = "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
) : ExtensionComponent {
|
||||||
|
@Transient var parent: LanguagePackExtension? = null
|
||||||
|
|
||||||
|
@SerialName("hanShapeBasedTable")
|
||||||
|
private val _hanShapeBasedTable: String? = null // Allows overriding the sqlite3 table to query in the json
|
||||||
|
val hanShapeBasedTable
|
||||||
|
get() = _hanShapeBasedTable ?: locale.variant
|
||||||
|
}
|
||||||
|
|
||||||
|
@SerialName(LanguagePackExtension.SERIAL_TYPE)
|
||||||
|
@Serializable
|
||||||
|
class LanguagePackExtension( // FIXME: how to make this support multiple types of language packs, and selectively load?
|
||||||
|
override val meta: ExtensionMeta,
|
||||||
|
override val dependencies: List<String>? = null,
|
||||||
|
val items: List<LanguagePackComponent> = listOf(),
|
||||||
|
val hanShapeBasedSQLite: String = "han.sqlite3",
|
||||||
|
) : Extension() {
|
||||||
|
|
||||||
|
override fun components(): List<ExtensionComponent> = items
|
||||||
|
|
||||||
|
override fun edit(): ExtensionEditor {
|
||||||
|
TODO("LOL LMAO")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SERIAL_TYPE = "ime.extension.languagepack"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialType() = SERIAL_TYPE
|
||||||
|
|
||||||
|
@Transient var hanShapeBasedSQLiteDatabase: SQLiteDatabase = SQLiteDatabase.create(null)
|
||||||
|
|
||||||
|
override fun onAfterLoad(context: Context, cacheDir: FsDir) {
|
||||||
|
// FIXME: this is loading language packs of all subtypes when they load.
|
||||||
|
super.onAfterLoad(context, cacheDir)
|
||||||
|
|
||||||
|
val databasePath = workingDir?.subFile(hanShapeBasedSQLite)?.path
|
||||||
|
if (databasePath == null) {
|
||||||
|
flogError { "Han shape-based language pack not found or loaded" }
|
||||||
|
} else try {
|
||||||
|
// TODO: use lock on database?
|
||||||
|
hanShapeBasedSQLiteDatabase.takeIf { it.isOpen }?.close()
|
||||||
|
hanShapeBasedSQLiteDatabase =
|
||||||
|
SQLiteDatabase.openDatabase(databasePath, null, SQLiteDatabase.OPEN_READONLY);
|
||||||
|
} catch (e: SQLiteException) {
|
||||||
|
flogError { "SQLiteException in openDatabase: path=$databasePath, error='${e}'" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeforeUnload(context: Context, cacheDir: FsDir) {
|
||||||
|
super.onBeforeUnload(context, cacheDir)
|
||||||
|
hanShapeBasedSQLiteDatabase.takeIf { it.isOpen }?.close()
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
package dev.patrickgold.florisboard.ime.nlp
|
package dev.patrickgold.florisboard.ime.nlp
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.icu.text.BreakIterator
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
@ -32,7 +33,9 @@ import dev.patrickgold.florisboard.editorInstance
|
|||||||
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
|
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
|
||||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||||
import dev.patrickgold.florisboard.ime.editor.EditorContent
|
import dev.patrickgold.florisboard.ime.editor.EditorContent
|
||||||
|
import dev.patrickgold.florisboard.ime.editor.EditorRange
|
||||||
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
|
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
|
||||||
import dev.patrickgold.florisboard.keyboardManager
|
import dev.patrickgold.florisboard.keyboardManager
|
||||||
import dev.patrickgold.florisboard.lib.devtools.flogError
|
import dev.patrickgold.florisboard.lib.devtools.flogError
|
||||||
import dev.patrickgold.florisboard.lib.kotlin.collectLatestIn
|
import dev.patrickgold.florisboard.lib.kotlin.collectLatestIn
|
||||||
@ -65,6 +68,7 @@ class NlpManager(context: Context) {
|
|||||||
private val providers = guardedByLock {
|
private val providers = guardedByLock {
|
||||||
mapOf(
|
mapOf(
|
||||||
LatinLanguageProvider.ProviderId to ProviderInstanceWrapper(LatinLanguageProvider(context)),
|
LatinLanguageProvider.ProviderId to ProviderInstanceWrapper(LatinLanguageProvider(context)),
|
||||||
|
HanShapeBasedLanguageProvider.ProviderId to ProviderInstanceWrapper(HanShapeBasedLanguageProvider(context)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +174,12 @@ class NlpManager(context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun determineLocalComposing(textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup): EditorRange {
|
||||||
|
return getSuggestionProvider(subtypeManager.activeSubtype).determineLocalComposing(
|
||||||
|
subtypeManager.activeSubtype, textBeforeSelection, breakIterators
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun suggest(subtype: Subtype, content: EditorContent) {
|
fun suggest(subtype: Subtype, content: EditorContent) {
|
||||||
val reqTime = SystemClock.uptimeMillis()
|
val reqTime = SystemClock.uptimeMillis()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.ime.nlp
|
package dev.patrickgold.florisboard.ime.nlp
|
||||||
|
|
||||||
|
import android.icu.text.BreakIterator
|
||||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||||
import dev.patrickgold.florisboard.ime.editor.EditorContent
|
import dev.patrickgold.florisboard.ime.editor.EditorContent
|
||||||
|
import dev.patrickgold.florisboard.ime.editor.EditorRange
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base interface for any NLP provider implementation. NLP providers maintain their own internal state and only receive
|
* Base interface for any NLP provider implementation. NLP providers maintain their own internal state and only receive
|
||||||
@ -193,6 +195,33 @@ interface SuggestionProvider : NlpProvider {
|
|||||||
* exist, 0.0 should be returned.
|
* exist, 0.0 should be returned.
|
||||||
*/
|
*/
|
||||||
suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double
|
suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When initializing composing text given a new context, the suggestion engine determines the composing range.
|
||||||
|
* The default behavior gets the last word according to the current subtype's primaryLocale.
|
||||||
|
* @param subtype The current subtype used to determine word or character boundary.
|
||||||
|
* @param textBeforeSelection The text whose end we want to compose.
|
||||||
|
* @param breakIterators cache of BreakIterator(s) to determine boundary.
|
||||||
|
*
|
||||||
|
* @return EditorRange indicating composing range.
|
||||||
|
*/
|
||||||
|
suspend fun determineLocalComposing(
|
||||||
|
subtype: Subtype,
|
||||||
|
textBeforeSelection: CharSequence,
|
||||||
|
breakIterators: BreakIteratorGroup
|
||||||
|
): EditorRange {
|
||||||
|
return breakIterators.word(subtype.primaryLocale) {
|
||||||
|
it.setText(textBeforeSelection.toString())
|
||||||
|
val end = it.last()
|
||||||
|
val isWord = it.ruleStatus != BreakIterator.WORD_NONE
|
||||||
|
if (isWord) {
|
||||||
|
val start = it.previous()
|
||||||
|
EditorRange(start, end)
|
||||||
|
} else {
|
||||||
|
EditorRange.Unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Patrick Goldinger
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.patrickgold.florisboard.ime.nlp.han
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteException
|
||||||
|
import android.icu.text.BreakIterator
|
||||||
|
import dev.patrickgold.florisboard.appContext
|
||||||
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
|
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||||
|
import dev.patrickgold.florisboard.ime.editor.EditorContent
|
||||||
|
import dev.patrickgold.florisboard.ime.editor.EditorRange
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.BreakIteratorGroup
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackComponent
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.SpellingProvider
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.SpellingResult
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.WordSuggestionCandidate
|
||||||
|
import dev.patrickgold.florisboard.lib.devtools.flogDebug
|
||||||
|
import dev.patrickgold.florisboard.lib.devtools.flogError
|
||||||
|
import dev.patrickgold.florisboard.subtypeManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class HanShapeBasedLanguageProvider(val context: Context) : SpellingProvider, SuggestionProvider {
|
||||||
|
companion object {
|
||||||
|
// Default user ID used for all subtypes, unless otherwise specified.
|
||||||
|
// See `ime/core/Subtype.kt` Line 210 and 211 for the default usage
|
||||||
|
const val ProviderId = "org.florisboard.nlp.providers.han.shape"
|
||||||
|
|
||||||
|
const val DB_PATH = "han.sqlite3";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val appContext by context.appContext()
|
||||||
|
|
||||||
|
private val maxFreqBySubType = mutableMapOf<String, Int>();
|
||||||
|
private val extensionManager by context.extensionManager()
|
||||||
|
private val subtypeManager by context.subtypeManager()
|
||||||
|
private val allLanguagePacks: List<LanguagePackExtension>
|
||||||
|
// Assume other types of extensions do not extend LanguagePackExtension
|
||||||
|
get() = extensionManager.languagePacks.value ?: listOf()
|
||||||
|
private var __connectedActiveLanguagePacks: Set<LanguagePackExtension> = setOf() // FIXME: hack for not able to observe extensionManager.languagePacks and subtypeManager.subtypes
|
||||||
|
private var languagePackItems: Map<String, LanguagePackComponent> = mapOf() // init in refreshLanguagePacks()
|
||||||
|
private var keyCode: Map<String, Set<Char>> = mapOf() // init in refreshLanguagePacks()
|
||||||
|
private val activeLanguagePacks // language packs referenced in subtypes
|
||||||
|
get() = buildSet {
|
||||||
|
val locales = subtypeManager.subtypes.map { it.primaryLocale.localeTag() }.toSet()
|
||||||
|
for (languagePack in allLanguagePacks) {
|
||||||
|
// FIXME: skip checking language pack type because it always is for now
|
||||||
|
if (languagePack.items.any { it.locale.localeTag() in locales }) {
|
||||||
|
add(languagePack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) // same as NlpManager's preload()
|
||||||
|
|
||||||
|
override val providerId = ProviderId
|
||||||
|
|
||||||
|
init {
|
||||||
|
// // FIXME: observeForever only callable on the main thread.
|
||||||
|
// extensionManager.languagePacks.observeForever { refreshLanguagePacks() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshLanguagePacks() {
|
||||||
|
scope.launch { create() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun create() {
|
||||||
|
// Here we initialize our provider, set up all things which are not language dependent.
|
||||||
|
// Refresh language pack parsing
|
||||||
|
|
||||||
|
// build index of available language packs
|
||||||
|
languagePackItems = buildMap {
|
||||||
|
for (languagePack in allLanguagePacks) {
|
||||||
|
// FIXME: skip checking language pack type because it always is for now
|
||||||
|
// if (languagePack is HanShapeBasedLanguagePackExtensionImpl)
|
||||||
|
for (languagePackItem in languagePack.items) {
|
||||||
|
put(languagePackItem.locale.localeTag(), languagePackItem)
|
||||||
|
// FIXME: how to put this in deserialization?
|
||||||
|
languagePackItem.parent = languagePack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
keyCode = buildMap {
|
||||||
|
languagePackItems.forEach { (tag, languagePackItem) ->
|
||||||
|
put(tag, languagePackItem.hanShapeBasedKeyCode.toSet())
|
||||||
|
}
|
||||||
|
put("default", "abcdefghijklmnopqrstuvwxyz".toSet())
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
// Load all actively used language packs.
|
||||||
|
val activeLanguagePacks = activeLanguagePacks
|
||||||
|
for (activeLanguagePack in activeLanguagePacks) {
|
||||||
|
if (!activeLanguagePack.isLoaded()) {
|
||||||
|
// populates activeLanguagePack.hanShapeBasedSQLiteDatabase
|
||||||
|
// FIXME: every time this is copied over to cache.
|
||||||
|
activeLanguagePack.load(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
__connectedActiveLanguagePacks = activeLanguagePacks
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun preload(subtype: Subtype) = withContext(Dispatchers.IO) {
|
||||||
|
// Here we have the chance to preload dictionaries and prepare a neural network for a specific language.
|
||||||
|
// Is kept in sync with the active keyboard subtype of the user, however a new preload does not necessary mean
|
||||||
|
// the previous language is not needed anymore (e.g. if the user constantly switches between two subtypes)
|
||||||
|
|
||||||
|
// To read a file from the APK assets the following methods can be used:
|
||||||
|
// appContext.assets.open()
|
||||||
|
// appContext.assets.reader()
|
||||||
|
// appContext.assets.bufferedReader()
|
||||||
|
// appContext.assets.readText()
|
||||||
|
// To copy an APK file/dir to the file system cache (appContext.cacheDir), the following methods are available:
|
||||||
|
// appContext.assets.copy()
|
||||||
|
// appContext.assets.copyRecursively()
|
||||||
|
|
||||||
|
// The subtype we get here contains a lot of data, however we are only interested in subtype.primaryLocale and
|
||||||
|
// subtype.secondaryLocales.
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun spell(
|
||||||
|
subtype: Subtype,
|
||||||
|
word: String,
|
||||||
|
precedingWords: List<String>,
|
||||||
|
followingWords: List<String>,
|
||||||
|
maxSuggestionCount: Int,
|
||||||
|
allowPossiblyOffensive: Boolean,
|
||||||
|
isPrivateSession: Boolean,
|
||||||
|
): SpellingResult {
|
||||||
|
return when (word.lowercase()) {
|
||||||
|
// Use typo for typing errors
|
||||||
|
"typo" -> SpellingResult.typo(arrayOf("typo1", "typo2", "typo3"))
|
||||||
|
// Use grammar error if the algorithm can detect this. On Android 11 and lower grammar errors are visually
|
||||||
|
// marked as typos due to a lack of support
|
||||||
|
"gerror" -> SpellingResult.grammarError(arrayOf("grammar1", "grammar2", "grammar3"))
|
||||||
|
// Use valid word for valid input
|
||||||
|
else -> SpellingResult.validWord()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggest(
|
||||||
|
subtype: Subtype,
|
||||||
|
content: EditorContent,
|
||||||
|
maxCandidateCount: Int,
|
||||||
|
allowPossiblyOffensive: Boolean,
|
||||||
|
isPrivateSession: Boolean,
|
||||||
|
): List<SuggestionCandidate> {
|
||||||
|
if (__connectedActiveLanguagePacks != activeLanguagePacks) {
|
||||||
|
// FIXME: hack for not able to observe extensionManager.languagePacks
|
||||||
|
refreshLanguagePacks()
|
||||||
|
}
|
||||||
|
if (content.composingText.isEmpty()) {
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return emptyList();
|
||||||
|
val layout: String = languagePackItem.hanShapeBasedTable
|
||||||
|
try {
|
||||||
|
val database = languagePackExtension.hanShapeBasedSQLiteDatabase
|
||||||
|
val cur = database.query(layout, arrayOf ( "code", "text" ), "code LIKE ? || '%'", arrayOf(content.composingText), "", "", "code ASC, weight DESC", "$maxCandidateCount");
|
||||||
|
cur.moveToFirst();
|
||||||
|
val rowCount = cur.getCount();
|
||||||
|
flogDebug { "Query was '${content.composingText}'" }
|
||||||
|
val suggestions = buildList {
|
||||||
|
for (n in 0 until rowCount) {
|
||||||
|
val code = cur.getString(0);
|
||||||
|
val word = cur.getString(1);
|
||||||
|
cur.moveToNext();
|
||||||
|
add(WordSuggestionCandidate(
|
||||||
|
text = "$word",
|
||||||
|
secondaryText = code,
|
||||||
|
confidence = 0.5,
|
||||||
|
isEligibleForAutoCommit = n == 0,
|
||||||
|
// We set ourselves as the source provider so we can get notify events for our candidate
|
||||||
|
sourceProvider = this@HanShapeBasedLanguageProvider,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return suggestions
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
flogError { "Invalid layout '${layout}' not found" }
|
||||||
|
return emptyList();
|
||||||
|
} catch (e: SQLiteException) {
|
||||||
|
flogError { "SQLiteException: layout=$layout, composing=${content.composingText}, error='${e}'" }
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) {
|
||||||
|
// We can use flogDebug, flogInfo, flogWarning and flogError for debug logging, which is a wrapper for Logcat
|
||||||
|
flogDebug { candidate.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) {
|
||||||
|
flogDebug { candidate.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeSuggestion(subtype: Subtype, candidate: SuggestionCandidate): Boolean {
|
||||||
|
flogDebug { candidate.toString() }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLanguagePack(subtype: Subtype): Pair<LanguagePackComponent, LanguagePackExtension>? {
|
||||||
|
val languagePackItem = languagePackItems[subtype.primaryLocale.localeTag()]
|
||||||
|
val languagePackExtension = languagePackItem?.parent
|
||||||
|
if (languagePackItem == null || languagePackExtension == null) {
|
||||||
|
flogError { "Could not read language pack item / extension" }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Pair(languagePackItem, languagePackExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListOfWords(subtype: Subtype): List<String> {
|
||||||
|
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return emptyList();
|
||||||
|
val layout: String = languagePackItem.hanShapeBasedTable
|
||||||
|
try {
|
||||||
|
val database = languagePackExtension.hanShapeBasedSQLiteDatabase
|
||||||
|
val cur = database.query(layout, arrayOf ( "text" ), "", arrayOf(), "", "", "weight DESC, code ASC", "");
|
||||||
|
cur.moveToFirst();
|
||||||
|
val rowCount = cur.getCount();
|
||||||
|
val suggestions = buildList {
|
||||||
|
for (n in 0 until rowCount) {
|
||||||
|
val word = cur.getString(0);
|
||||||
|
cur.moveToNext();
|
||||||
|
add(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flogDebug { "Read ${suggestions.size} words for ${subtype.primaryLocale.localeTag()}" }
|
||||||
|
return suggestions;
|
||||||
|
} catch (e: SQLiteException) {
|
||||||
|
flogError { "Encountered an SQL error: ${e}" }
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double {
|
||||||
|
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return 0.0;
|
||||||
|
val layout: String = languagePackItem.hanShapeBasedTable
|
||||||
|
try {
|
||||||
|
val database = languagePackExtension.hanShapeBasedSQLiteDatabase
|
||||||
|
val cur = database.query(layout, arrayOf ( "weight" ), "code = ?", arrayOf(word), "", "", "", "");
|
||||||
|
cur.moveToFirst();
|
||||||
|
return try { cur.getDouble(0) } catch (e: Exception) { 0.0 };
|
||||||
|
} catch (e: SQLiteException) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun destroy() {
|
||||||
|
// Here we have the chance to de-allocate memory and finish our work. However this might never be called if
|
||||||
|
// the app process is killed (which will most likely always be the case).
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun determineLocalComposing(subtype: Subtype, textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup): EditorRange {
|
||||||
|
return breakIterators.character(subtype.primaryLocale) {
|
||||||
|
it.setText(textBeforeSelection.toString())
|
||||||
|
val end = it.last()
|
||||||
|
var start = end
|
||||||
|
var next = it.previous()
|
||||||
|
val keyCodeLocale = keyCode[subtype.primaryLocale.localeTag()]?: keyCode["default"]?: emptySet()
|
||||||
|
while (next != BreakIterator.DONE) {
|
||||||
|
val sub = textBeforeSelection.substring(next, start)
|
||||||
|
if (! sub.all { char -> char in keyCodeLocale })
|
||||||
|
break
|
||||||
|
start = next
|
||||||
|
next = it.previous()
|
||||||
|
}
|
||||||
|
if (start != end) {
|
||||||
|
flogDebug { "Determined $start - $end as composing: ${textBeforeSelection.substring(start, end)}" }
|
||||||
|
EditorRange(start, end)
|
||||||
|
} else {
|
||||||
|
flogDebug { "Determined Unspecified as composing" }
|
||||||
|
EditorRange.Unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.lib
|
package dev.patrickgold.florisboard.lib
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dev.patrickgold.florisboard.extensionManager
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
import dev.patrickgold.florisboard.lib.kotlin.titlecase
|
import dev.patrickgold.florisboard.lib.kotlin.titlecase
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -125,6 +128,31 @@ class FlorisLocale private constructor(val base: Locale) {
|
|||||||
* @see java.util.Locale.getAvailableLocales
|
* @see java.util.Locale.getAvailableLocales
|
||||||
*/
|
*/
|
||||||
fun installedSystemLocales(): List<FlorisLocale> = Locale.getAvailableLocales().map { from(it) }
|
fun installedSystemLocales(): List<FlorisLocale> = Locale.getAvailableLocales().map { from(it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all installed locales and custom locales.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun extendedAvailableLocales(context: Context): List<FlorisLocale> {
|
||||||
|
val systemLocales = installedSystemLocales()
|
||||||
|
val extensionManager by context.extensionManager()
|
||||||
|
val systemLocalesSet = buildSet {
|
||||||
|
for (locale in systemLocales) {
|
||||||
|
add(locale.localeTag())
|
||||||
|
}
|
||||||
|
}.toSet()
|
||||||
|
val extraLocales = buildList {
|
||||||
|
for (languagePackExtension in extensionManager.languagePacks.value ?: listOf()) {
|
||||||
|
for (languagePackItem in languagePackExtension.items) {
|
||||||
|
val locale = languagePackItem.locale
|
||||||
|
if (from(locale.language, locale.country).localeTag() in systemLocalesSet) {
|
||||||
|
add(locale.localeTag())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toSet()
|
||||||
|
return systemLocales + extraLocales.map { fromTag(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,6 +223,16 @@ class FlorisLocale private constructor(val base: Locale) {
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if suggestions in this language should have spaces added after, false otherwise.
|
||||||
|
* TODO: this is absolutely not exhaustive and hard-coded, find solution based on ICU or system
|
||||||
|
*/
|
||||||
|
val supportsAutoSpace: Boolean
|
||||||
|
get() = when (language) {
|
||||||
|
"zh", "ko", "jp", "th" -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the language tag for this locale in the format `xx`,
|
* Generates the language tag for this locale in the format `xx`,
|
||||||
* `xx-YY` or `xx-YY-zzz` and returns it as a string.
|
* `xx-YY` or `xx-YY-zzz` and returns it as a string.
|
||||||
|
@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import dev.patrickgold.florisboard.appContext
|
import dev.patrickgold.florisboard.appContext
|
||||||
import dev.patrickgold.florisboard.assetManager
|
import dev.patrickgold.florisboard.assetManager
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||||
|
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
|
||||||
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
||||||
import dev.patrickgold.florisboard.ime.text.composing.Composer
|
import dev.patrickgold.florisboard.ime.text.composing.Composer
|
||||||
import dev.patrickgold.florisboard.ime.text.composing.HangulUnicode
|
import dev.patrickgold.florisboard.ime.text.composing.HangulUnicode
|
||||||
@ -62,6 +63,7 @@ val ExtensionJsonConfig = Json {
|
|||||||
polymorphic(Extension::class) {
|
polymorphic(Extension::class) {
|
||||||
subclass(KeyboardExtension::class, KeyboardExtension.serializer())
|
subclass(KeyboardExtension::class, KeyboardExtension.serializer())
|
||||||
subclass(ThemeExtension::class, ThemeExtension.serializer())
|
subclass(ThemeExtension::class, ThemeExtension.serializer())
|
||||||
|
subclass(LanguagePackExtension::class, LanguagePackExtension.serializer())
|
||||||
}
|
}
|
||||||
polymorphic(Composer::class) {
|
polymorphic(Composer::class) {
|
||||||
subclass(Appender::class, Appender.serializer())
|
subclass(Appender::class, Appender.serializer())
|
||||||
@ -77,6 +79,7 @@ class ExtensionManager(context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
const val IME_KEYBOARD_PATH = "ime/keyboard"
|
const val IME_KEYBOARD_PATH = "ime/keyboard"
|
||||||
const val IME_THEME_PATH = "ime/theme"
|
const val IME_THEME_PATH = "ime/theme"
|
||||||
|
const val IME_LANGUAGEPACK_PATH = "ime/languagepack"
|
||||||
|
|
||||||
private const val FILE_OBSERVER_MASK =
|
private const val FILE_OBSERVER_MASK =
|
||||||
FileObserver.CLOSE_WRITE or FileObserver.DELETE or FileObserver.MOVED_FROM or FileObserver.MOVED_TO
|
FileObserver.CLOSE_WRITE or FileObserver.DELETE or FileObserver.MOVED_FROM or FileObserver.MOVED_TO
|
||||||
@ -88,10 +91,12 @@ class ExtensionManager(context: Context) {
|
|||||||
|
|
||||||
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
|
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
|
||||||
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH)
|
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH)
|
||||||
|
val languagePacks = ExtensionIndex(LanguagePackExtension.serializer(), IME_LANGUAGEPACK_PATH)
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
keyboardExtensions.init()
|
keyboardExtensions.init()
|
||||||
themes.init()
|
themes.init()
|
||||||
|
languagePacks.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun import(ext: Extension) {
|
fun import(ext: Extension) {
|
||||||
@ -100,6 +105,7 @@ class ExtensionManager(context: Context) {
|
|||||||
val relGroupPath = when (ext) {
|
val relGroupPath = when (ext) {
|
||||||
is KeyboardExtension -> IME_KEYBOARD_PATH
|
is KeyboardExtension -> IME_KEYBOARD_PATH
|
||||||
is ThemeExtension -> IME_THEME_PATH
|
is ThemeExtension -> IME_THEME_PATH
|
||||||
|
is LanguagePackExtension -> IME_LANGUAGEPACK_PATH
|
||||||
else -> error("Unknown extension type")
|
else -> error("Unknown extension type")
|
||||||
}
|
}
|
||||||
ext.sourceRef = FlorisRef.internal(relGroupPath).subRef(extFileName)
|
ext.sourceRef = FlorisRef.internal(relGroupPath).subRef(extFileName)
|
||||||
@ -125,6 +131,7 @@ class ExtensionManager(context: Context) {
|
|||||||
fun getExtensionById(id: String): Extension? {
|
fun getExtensionById(id: String): Extension? {
|
||||||
keyboardExtensions.value?.find { it.meta.id == id }?.let { return it }
|
keyboardExtensions.value?.find { it.meta.id == id }?.let { return it }
|
||||||
themes.value?.find { it.meta.id == id }?.let { return it }
|
themes.value?.find { it.meta.id == id }?.let { return it }
|
||||||
|
languagePacks.value?.find { it.meta.id == id }?.let { return it }
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,10 +101,13 @@
|
|||||||
<string name="settings__localization__display_language_names_in__label" comment="Label of Display language names in preference">Display language names in</string>
|
<string name="settings__localization__display_language_names_in__label" comment="Label of Display language names in preference">Display language names in</string>
|
||||||
<string name="settings__localization__group_subtypes__label" comment="Label of subtypes group">Subtypes</string>
|
<string name="settings__localization__group_subtypes__label" comment="Label of subtypes group">Subtypes</string>
|
||||||
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Add subtype</string>
|
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Add subtype</string>
|
||||||
|
<string name="settings__localization__language_pack_title" comment="Title of the language pack manager screen for managing installed and custom language packs">Manage installed language packs</string>
|
||||||
|
<string name="settings__localization__language_pack_summary" comment="Summary of preference item for adding a new language pack">Experimental: manage extensions that add support for specific languages (shape-based Chinese input for now)</string>
|
||||||
<string name="settings__localization__subtype_edit_title" comment="Title of subtype dialog when editing an existing subtype">Edit subtype</string>
|
<string name="settings__localization__subtype_edit_title" comment="Title of subtype dialog when editing an existing subtype">Edit subtype</string>
|
||||||
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">Primary language</string>
|
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">Primary language</string>
|
||||||
<string name="settings__localization__subtype_popup_mapping" comment="Label for popup mapping dropdown in subtype screen">Popup mapping</string>
|
<string name="settings__localization__subtype_popup_mapping" comment="Label for popup mapping dropdown in subtype screen">Popup mapping</string>
|
||||||
<string name="settings__localization__subtype_characters_layout" comment="Label for layout dropdown in subtype dialog">Characters layout</string>
|
<string name="settings__localization__subtype_characters_layout" comment="Label for layout dropdown in subtype dialog">Characters layout</string>
|
||||||
|
<string name="settings__localization__subtype_suggestion_provider" comment="Label for suggestion provider dropdown in subtype dialog">Suggestion engine</string>
|
||||||
<string name="settings__localization__subtype_symbols_layout" comment="Label for layout dropdown in subtype dialog">Symbols primary layout</string>
|
<string name="settings__localization__subtype_symbols_layout" comment="Label for layout dropdown in subtype dialog">Symbols primary layout</string>
|
||||||
<string name="settings__localization__subtype_symbols2_layout" comment="Label for layout dropdown in subtype dialog">Symbols secondary layout</string>
|
<string name="settings__localization__subtype_symbols2_layout" comment="Label for layout dropdown in subtype dialog">Symbols secondary layout</string>
|
||||||
<string name="settings__localization__subtype_composer" comment="Label for composer dropdown in subtype dialog.">Composer</string>
|
<string name="settings__localization__subtype_composer" comment="Label for composer dropdown in subtype dialog.">Composer</string>
|
||||||
@ -599,6 +602,7 @@
|
|||||||
<string name="ext__meta__authors">Authors</string>
|
<string name="ext__meta__authors">Authors</string>
|
||||||
<string name="ext__meta__components">Bundled components</string>
|
<string name="ext__meta__components">Bundled components</string>
|
||||||
<string name="ext__meta__components_theme">Bundled themes</string>
|
<string name="ext__meta__components_theme">Bundled themes</string>
|
||||||
|
<string name="ext__meta__components_language_pack">Bundled language packs</string>
|
||||||
<string name="ext__meta__components_none_found">This extension archive does not contain any bundled components.</string>
|
<string name="ext__meta__components_none_found">This extension archive does not contain any bundled components.</string>
|
||||||
<string name="ext__meta__description">Description</string>
|
<string name="ext__meta__description">Description</string>
|
||||||
<string name="ext__meta__homepage">Homepage</string>
|
<string name="ext__meta__homepage">Homepage</string>
|
||||||
@ -638,6 +642,7 @@
|
|||||||
<string name="ext__import__ext_any" comment="Title of Importer screen for import of any supported FlorisBoard extension">Import extension</string>
|
<string name="ext__import__ext_any" comment="Title of Importer screen for import of any supported FlorisBoard extension">Import extension</string>
|
||||||
<string name="ext__import__ext_keyboard" comment="Title of Importer screen for keyboard extension import">Import keyboard extension</string>
|
<string name="ext__import__ext_keyboard" comment="Title of Importer screen for keyboard extension import">Import keyboard extension</string>
|
||||||
<string name="ext__import__ext_theme" comment="Title of Importer screen for theme extension import">Import theme extension</string>
|
<string name="ext__import__ext_theme" comment="Title of Importer screen for theme extension import">Import theme extension</string>
|
||||||
|
<string name="ext__import__ext_languagepack" comment="Title of Importer screen for language pack extension import">Import language pack extension</string>
|
||||||
<string name="ext__import__file_skip" comment="Label when a file cannot be imported in the current context. The actual reason string is in a separate text view below this string.">File can not be imported. Reason:</string>
|
<string name="ext__import__file_skip" comment="Label when a file cannot be imported in the current context. The actual reason string is in a separate text view below this string.">File can not be imported. Reason:</string>
|
||||||
<string name="ext__import__file_skip_unsupported" comment="Reason string when file is unsupported">Unsupported or unrecognized file type.</string>
|
<string name="ext__import__file_skip_unsupported" comment="Reason string when file is unsupported">Unsupported or unrecognized file type.</string>
|
||||||
<string name="ext__import__file_skip_ext_core" comment="Reason string when ext has core extension ID">Unable to replace or update default extension packages provided with the app core assets. Consider updating the app itself if you intend to use a newer version of a core extension package.</string>
|
<string name="ext__import__file_skip_ext_core" comment="Reason string when ext has core extension ID">Unable to replace or update default extension packages provided with the app core assets. Consider updating the app itself if you intend to use a newer version of a core extension package.</string>
|
||||||
|
42
flake.lock
Normal file
42
flake.lock
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1667395993,
|
||||||
|
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1673387755,
|
||||||
|
"narHash": "sha256-7y2wuml3bnR2jRgSpDTdeKx9HeAuGW7rYpk4G+Ibeuk=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "ce1aa29621356706746c53e2d480da7c68f6c972",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
81
flake.nix
Normal file
81
flake.nix
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
description = "florisboard build environment";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}: let
|
||||||
|
supportedSystems = with flake-utils.lib.system; [x86_64-linux x86_64-darwin aarch64-darwin];
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
|
||||||
|
android = {
|
||||||
|
versions = {
|
||||||
|
tools = "26.1.1";
|
||||||
|
platformTools = "33.0.3";
|
||||||
|
buildTools = "31.0.0";
|
||||||
|
ndk = "22.1.7171670";
|
||||||
|
cmake = "3.18.1";
|
||||||
|
emulator = "31.3.9";
|
||||||
|
};
|
||||||
|
platforms = ["32"];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
flake-utils.lib.eachSystem supportedSystems
|
||||||
|
(
|
||||||
|
system: let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config = {
|
||||||
|
android_sdk.accept_license = true; # accept all of the sdk licenses
|
||||||
|
allowUnfree = true; # needed to get android stuff to compile
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# Make the android enviornment we specify
|
||||||
|
android-composition = pkgs.androidenv.composeAndroidPackages {
|
||||||
|
toolsVersion = android.versions.tools;
|
||||||
|
platformToolsVersion = android.versions.platformTools;
|
||||||
|
buildToolsVersions = [android.versions.buildTools];
|
||||||
|
platformVersions = android.platforms;
|
||||||
|
cmakeVersions = [android.versions.cmake];
|
||||||
|
includeNDK = true;
|
||||||
|
ndkVersions = [android.versions.ndk];
|
||||||
|
includeEmulator = true;
|
||||||
|
emulatorVersion = android.versions.emulator;
|
||||||
|
};
|
||||||
|
android-sdk =
|
||||||
|
(pkgs.androidenv.composeAndroidPackages {
|
||||||
|
toolsVersion = android.versions.tools;
|
||||||
|
platformToolsVersion = android.versions.platformTools;
|
||||||
|
buildToolsVersions = [android.versions.buildTools];
|
||||||
|
platformVersions = android.platforms;
|
||||||
|
cmakeVersions = [android.versions.cmake];
|
||||||
|
includeNDK = true;
|
||||||
|
ndkVersions = [android.versions.ndk];
|
||||||
|
})
|
||||||
|
.androidsdk;
|
||||||
|
in rec {
|
||||||
|
devShells.default = pkgs.mkShell rec {
|
||||||
|
ANDROID_SDK_ROOT = "${android-sdk}/libexec/android-sdk";
|
||||||
|
ANDROID_NDK_ROOT = "${ANDROID_SDK_ROOT}/ndk-bundle";
|
||||||
|
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${android.versions.buildTools}/aapt2";
|
||||||
|
JAVA_HOME = "${pkgs.jdk8.home}";
|
||||||
|
nativeBulidInputs = with pkgs; [
|
||||||
|
android-sdk
|
||||||
|
jdk8
|
||||||
|
clang
|
||||||
|
kotlin-language-server
|
||||||
|
];
|
||||||
|
|
||||||
|
# Use the same cmakeVersion here
|
||||||
|
shellHook = ''
|
||||||
|
export PATH="$(echo "$ANDROID_SDK_ROOT/cmake/${android.versions.cmake}".*/bin):$PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
219
utils/convert_fcitx5_sqlite.py
Normal file
219
utils/convert_fcitx5_sqlite.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Execute in this folder to convert to sqlite:
|
||||||
|
# python3 convert_fcitx5_sqlite.py
|
||||||
|
# Or for a subset of tables only:
|
||||||
|
# python3 convert_fcitx5_sqlite.py cangjie-large.txt quick-classic.txt wubi-large.txt zhengma.txt
|
||||||
|
# https://github.com/fcitx/fcitx5-table-extra/tree/master/tables
|
||||||
|
# The tables are in public domain per their README.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
import sqlite3
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
def put_table(database, schema, table):
|
||||||
|
length, table = table['LengthReal'], table['Data']
|
||||||
|
assert re.fullmatch('[a-zA-Z0-9_]+', schema) is not None
|
||||||
|
columns = len(table[0])
|
||||||
|
assert all(len(x) == columns for x in table)
|
||||||
|
with sqlite3.connect(database) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
if columns == 3:
|
||||||
|
cur.execute(f'create table {schema}(code VARCHAR({length}), text TEXT, weight INT)')
|
||||||
|
cur.executemany(f'insert into {schema} values(?, ?, ?)', table)
|
||||||
|
elif columns == 4:
|
||||||
|
# hard-coded 5-long stem
|
||||||
|
length_stem = max(len(x[3]) for x in table if x[3] is not None)
|
||||||
|
cur.execute(f'create table {schema}(code VARCHAR({length}), text TEXT, weight INT, stem VARCHAR({length_stem}))')
|
||||||
|
cur.executemany(f'insert into {schema} values(?, ?, ?, ?)', table)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Number of columns ({columns}) not supported')
|
||||||
|
|
||||||
|
|
||||||
|
fcitx_fields_translate = {
|
||||||
|
'组词规则': 'Rule',
|
||||||
|
'数据': 'Data',
|
||||||
|
'提示': 'Prompt',
|
||||||
|
'拼音长度': 'PinyinLength',
|
||||||
|
'键码': 'KeyCode',
|
||||||
|
'拼音': 'Pinyin',
|
||||||
|
'码长': 'Length',
|
||||||
|
'构词': 'ConstructPhrase',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_fcitx_table(table):
|
||||||
|
with open(table, 'rt') as f:
|
||||||
|
lines = [line.strip('\n') for line in f.readlines()]
|
||||||
|
parsed = dict()
|
||||||
|
field_now = ''
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
if '\ufeff' in line:
|
||||||
|
line = line.replace('\ufeff', '')
|
||||||
|
if not line or line.startswith(';'):
|
||||||
|
continue
|
||||||
|
if line.startswith('[') and line.endswith(']'):
|
||||||
|
# starting a table
|
||||||
|
field_now = line[1:-1]
|
||||||
|
field_now = fcitx_fields_translate.get(field_now, field_now)
|
||||||
|
table_now = parsed[field_now] = []
|
||||||
|
else:
|
||||||
|
if field_now:
|
||||||
|
# appending to a table
|
||||||
|
if field_now == 'Data':
|
||||||
|
# Parse first ' ' or '\t' as splitting point.
|
||||||
|
# Assume ' ' and '\t' may be in the text.
|
||||||
|
split = len(line)
|
||||||
|
for x in ' \t':
|
||||||
|
try:
|
||||||
|
split = min(split, line.index(x))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if split == len(line):
|
||||||
|
print(f'Throwing away row with one column:')
|
||||||
|
print(repr(line))
|
||||||
|
line = None
|
||||||
|
else:
|
||||||
|
line = (line[:split], line[split+1:])
|
||||||
|
# elif field_now == 'Rule':
|
||||||
|
else:
|
||||||
|
line = line.split('=')
|
||||||
|
assert len(line) == 2
|
||||||
|
# else:
|
||||||
|
# raise ValueError(f'Table field {field_now} not recognized')
|
||||||
|
if line is not None:
|
||||||
|
table_now.append(line)
|
||||||
|
else:
|
||||||
|
# parsing other settings
|
||||||
|
assert '=' in line, f'{table} has line without "=":\n{line}'
|
||||||
|
split = line.index('=')
|
||||||
|
field = line[:split]
|
||||||
|
field = fcitx_fields_translate.get(field, field)
|
||||||
|
parsed[field] = line[split+1:]
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def clean_fcitx_table(table):
|
||||||
|
# process Data with special field.
|
||||||
|
out = dict(table)
|
||||||
|
|
||||||
|
# compute actual KeyCode used.
|
||||||
|
keycode_real = set()
|
||||||
|
for x in out['Data']:
|
||||||
|
keycode_real |= set(x[0])
|
||||||
|
|
||||||
|
# Prompt: just add to word list and KeyCode.
|
||||||
|
if 'Prompt' in out and out['Prompt'] in keycode_real:
|
||||||
|
out['KeyCode'] += out['Prompt']
|
||||||
|
# Pinyin: just add to word list and KeyCode.
|
||||||
|
if 'Pinyin' in out and out['Pinyin'] in keycode_real:
|
||||||
|
out['KeyCode'] += out['Pinyin']
|
||||||
|
# ConstructPhrase: add to "stem" column. (for zhengma_large)
|
||||||
|
if 'ConstructPhrase' in out and out['ConstructPhrase'] in keycode_real:
|
||||||
|
conchar = out['ConstructPhrase']
|
||||||
|
# separate constructing and non-constructing parts of the table
|
||||||
|
table_noncon = [x for x in out['Data'] if conchar not in x[0]]
|
||||||
|
table_con = [(x[0][1:], x[1]) for x in out['Data'] if conchar in x[0]]
|
||||||
|
# do a join on text
|
||||||
|
dict_con = {x[1]: x[0] for x in table_con}
|
||||||
|
assert len(table_con) == len(dict_con), \
|
||||||
|
'ConstructPhrase entries not unique'
|
||||||
|
assert all(not conchar in x for x in dict_con.values()), \
|
||||||
|
'ConstructPhrase appearing after starts'
|
||||||
|
out['Data'] = [(x[0], x[1], dict_con.get(x[1], None))
|
||||||
|
for x in table_noncon]
|
||||||
|
|
||||||
|
# Weight: just use order.
|
||||||
|
counter = collections.Counter(x[0] for x in out['Data'])
|
||||||
|
for idx, x in enumerate(out['Data']):
|
||||||
|
weight = counter[x[0]]
|
||||||
|
counter.subtract((x[0],))
|
||||||
|
x = x[:2] + (weight,) + x[2:]
|
||||||
|
out['Data'][idx] = x
|
||||||
|
assert not len(list(counter.elements()))
|
||||||
|
|
||||||
|
# compute KeyCodeReal one more time after trimming table
|
||||||
|
keycode_real = set()
|
||||||
|
for x in out['Data']:
|
||||||
|
keycode_real |= set(x[0])
|
||||||
|
out['KeyCodeReal'] = keycode_real
|
||||||
|
|
||||||
|
# actual seek length
|
||||||
|
out['LengthReal'] = max(len(x[0]) for x in out['Data'])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# Loading
|
||||||
|
tables = dict()
|
||||||
|
file_list = sys.argv[1:] if len(sys.argv) > 1 else glob.glob('[a-z]*.txt')
|
||||||
|
assert all(x.endswith('.txt') for x in file_list)
|
||||||
|
for x in file_list:
|
||||||
|
print(f'Processing {x}...')
|
||||||
|
schema = x[:-4].replace('-', '').replace('_', '')
|
||||||
|
tables[schema] = parse_fcitx_table(x)
|
||||||
|
conf = parse_fcitx_table(x[:-4] + '.conf.in')
|
||||||
|
conf = {k: {x[0]: x[1] for x in v} for k, v in conf.items()}
|
||||||
|
tables[schema]['.conf.in'] = conf
|
||||||
|
tables[schema]['FlorisLocale'] = f"{conf['InputMethod']['LangCode']}_{schema}"
|
||||||
|
|
||||||
|
# Fixing
|
||||||
|
if 'wubi98_pinyin' in tables:
|
||||||
|
tables['wubi98pinyin']['KeyCode'] += 'z'
|
||||||
|
keycode = set(tables['wubi98pinyin']['KeyCode']) | set(tables['wubi98pinyin']['Pinyin'])
|
||||||
|
for idx, x in enumerate(tables['wubi98pinyin']['Data']):
|
||||||
|
if not all(ch in keycode for ch in x[0]):
|
||||||
|
x = list(x)
|
||||||
|
x[0] = ''.join(ch for ch in x[0] if ch in keycode)
|
||||||
|
tables['wubi98pinyin']['Data'][idx] = tuple(x)
|
||||||
|
if 'easylarge' in tables:
|
||||||
|
tables['easylarge']['KeyCode'] += '|'
|
||||||
|
|
||||||
|
# Cleaning
|
||||||
|
for schema, table in tables.items():
|
||||||
|
print(f'Cleaning {schema}, with {len(table["Data"])} items...', end='')
|
||||||
|
tables[schema] = clean_fcitx_table(table)
|
||||||
|
print(f' Done, with {len(tables[schema]["Data"])} items.')
|
||||||
|
|
||||||
|
# Analysis
|
||||||
|
if True:
|
||||||
|
for schema, table in tables.items():
|
||||||
|
print(f'Analyzing {schema}... LengthReal = {table["LengthReal"]}')
|
||||||
|
specials = ["Prompt", "Pinyin", "ConstructPhrase"]
|
||||||
|
for field in specials:
|
||||||
|
if field in table:
|
||||||
|
has = [x for x in table['Data'] if table[field] in x[0]]
|
||||||
|
if has:
|
||||||
|
print(f'There are {len(has)}/{len(table["Data"])} with {field}={table[field]}')
|
||||||
|
keycode = set(table['KeyCode'])
|
||||||
|
keycode_real = set(table['KeyCodeReal'])
|
||||||
|
if keycode != keycode_real:
|
||||||
|
print(f'KeyCode mismatch:')
|
||||||
|
print(f'Claimed not used: ' + ''.join(sorted(keycode - keycode_real)))
|
||||||
|
print(f'Exists unclaimed: ' + ''.join(sorted(keycode_real - keycode)))
|
||||||
|
|
||||||
|
# Writing
|
||||||
|
language_pack = [dict(id=table['FlorisLocale'], hanShapeBasedKeyCode=table['KeyCode']) for schema, table in tables.items()]
|
||||||
|
with open('./extension-draft.json', 'wt') as f:
|
||||||
|
json.dump({'$': 'ime.extension.languagepack', 'items': sorted(language_pack, key=lambda x: x['id'])}, f, indent=2)
|
||||||
|
database = './han.sqlite3'
|
||||||
|
if os.path.exists(database):
|
||||||
|
os.remove(database)
|
||||||
|
for schema, table in tables.items():
|
||||||
|
put_table(database, schema, table)
|
||||||
|
# put_table(database, table['FlorisLocale'], table)
|
||||||
|
print({schema: table['KeyCode'] for schema, table in tables.items()})
|
||||||
|
|
||||||
|
# Final display
|
||||||
|
with sqlite3.connect(database) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
# for schema in ['zh_CN_zhengmapinyin', 'zh_CN_zhengmalarge', 'zh_CN_wubilarge', 'zh_CN_wubi98', 'zh_TW_cangjie5', 'zh_HK_stroke5']:
|
||||||
|
for schema in ['zhengmapinyin', 'zhengmalarge', 'wubilarge', 'wubi98', 'cangjie5', 'stroke5']:
|
||||||
|
if schema not in tables: continue
|
||||||
|
cur.execute(f'select * from {schema} order by length(code) desc')
|
||||||
|
print(cur.fetchmany(10))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user