From fbbd73cb81451fd42c097dd58e492246a9d52e4f Mon Sep 17 00:00:00 2001 From: fxliang Date: Mon, 23 Jun 2025 06:25:14 +0000 Subject: [PATCH 01/20] ci: add ci.yaml with personal prebuilt and fcitx5-rime --- .github/workflows/ci.yml | 138 ++++++++++++++++++++++++++++++++++++++ prepare_personal_build.sh | 18 +++++ 2 files changed, 156 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100755 prepare_personal_build.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..27a1b59c3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,138 @@ +name: Commit CI + +on: + workflow_dispatch: + push: + branches: + - '*' + tags: + - '![0-9]+.*' + paths: + - '**/**' + - '!*.md' + - '!.gitignore' + +jobs: + build_commit: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-22.04 + #- macos-13 + #- windows-2022 + abi: + #- armeabi-v7a + - arm64-v8a + #- x86 + #- x86_64 + env: + BUILD_ABI: ${{ matrix.abi }} + steps: + - name: Fetch source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + #- name: Regenerate symlinks pointing to submodule (Windows) + # if: ${{ matrix.os == 'windows-2022' }} + # run: | + # Remove-Item -Recurse app/src/main/assets/usr/share + # git checkout -- * + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Setup Android environment + uses: android-actions/setup-android@v3 + + - name: Install Android NDK + run: | + sdkmanager --install "cmake;3.22.1" + + - name: Install system dependencies (Ubuntu) + if: ${{ matrix.os == 'ubuntu-22.04' }} + run: | + sudo apt update + sudo apt install extra-cmake-modules gettext + + #- name: Install system dependencies (macOS) + # if: ${{ matrix.os == 'macos-13' }} + # run: | + # brew install extra-cmake-modules + + #- name: Install system dependencies (Windows) + # if: ${{ matrix.os == 'windows-2022' }} + # run: | + # C:/msys64/usr/bin/pacman -S --noconfirm mingw-w64-ucrt-x86_64-gettext mingw-w64-ucrt-x86_64-extra-cmake-modules + # Add-Content $env:GITHUB_PATH "C:/msys64/ucrt64/bin" + + - name: Prepare personal build sources + run: | + ./prepare_personal_build.sh + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Build Release APK + run: | + ./gradlew :app:assembleRelease + ./gradlew :assembleReleasePlugins + + # move plugin apks to app/build/outputs/apk/release + - name: Move plugin APKs + shell: bash + run: | + for i in $(ls plugin) + do + if [ -d "plugin/${i}" ] + then + mv "plugin/${i}/build/outputs/apk/release"/*.apk "app/build/outputs/apk/release/" + fi + done + + - name: Sign all App + uses: kevin-david/zipalign-sign-android-release@v1.1.1 + id: sign_apk + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + alias: ${{ secrets.KEY_ALIAS }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + zipAlign: true + env: + BUILD_TOOLS_VERSION: "31.0.0" + + - name: Delete unsigned APKs + shell: bash + run: | + rm app/build/outputs/apk/release/*unsigned.apk + pushd app/build/outputs/apk/release + for file in *unsigned-*; do + new_name="${file/unsigned-/}" + mv -- "$file" "$new_name" + done + popd + + - name: Upload app with plugins + uses: actions/upload-artifact@v4 + with: + name: app-${{ matrix.os }}-${{ matrix.abi }}-with-plugins + path: app/build/outputs/apk/release/*.apk + + # create nightly release + - name: Create Nightly release + uses: 'marvinpinto/action-automatic-releases@latest' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + automatic_release_tag: latest + prerelease: true + title: "Nightly Build" + files: | + app/build/outputs/apk/release/*.apk + diff --git a/prepare_personal_build.sh b/prepare_personal_build.sh new file mode 100755 index 000000000..07c88f63e --- /dev/null +++ b/prepare_personal_build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/bash + +# update fcitx5-rime +echo "updating fcitx5-rime" +pushd plugin/rime/src/main/cpp/fcitx5-rime +git remote add gh https://github.com/fxliang/fcitx5-rime.git || git remote set-url gh https://github.com/fxliang/fcitx5-rime.git +git fetch -v gh master +git checkout gh/master +popd +sed -i 's|/fcitx/|/fxliang/|g' plugin/rime/licenses/libraries/fcitx5-rime.json + +# update prebuilt +echo "updating prebuilt" +pushd lib/fcitx5/src/main/cpp/prebuilt +git remote add gh https://github.com/fxliang/prebuilt.git || git remote set-url gh https://github.com/fxliang/prebuilt.git +git fetch -v gh master +git checkout gh/master +popd From dd7180254111a69eed141710569b8794569aebb7 Mon Sep 17 00:00:00 2001 From: fxliang Date: Mon, 12 May 2025 09:26:26 +0000 Subject: [PATCH 02/20] feat: load Popup from json config config/PopupPreset.json --- .../android/input/popup/PopupComponent.kt | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt index 1d8ffe161..fe57c98f0 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt @@ -27,6 +27,9 @@ import splitties.views.dsl.core.add import splitties.views.dsl.core.frameLayout import splitties.views.dsl.core.lParams import java.util.LinkedList +import kotlinx.serialization.json.Json +import org.fcitx.fcitx5.android.utils.appContext +import java.io.File class PopupComponent : UniqueComponent(), Dependent, ManagedHandler by managedHandler() { @@ -62,6 +65,32 @@ class PopupComponent : private val rootLocation = intArrayOf(0, 0) private val rootBounds: Rect = Rect() + companion object { + private var lastModified = 0L + private var cachedPopupPreset: Map>? = null + val popupPresetJson: Map>? + @Synchronized + get() { + var file = File(appContext.getExternalFilesDir(null), "config/PopupPreset.json") + if (!file.exists()) { + cachedPopupPreset = null + return null + } + if (cachedPopupPreset == null || file.lastModified() != lastModified) { + try { + lastModified = file.lastModified() + val json = file.readText() + cachedPopupPreset = Json.decodeFromString>>(json) + .mapValues { it.value.toTypedArray() } + } catch (e: Exception) { + e.printStackTrace() + cachedPopupPreset = null + } + } + return cachedPopupPreset + } + } + val root by lazy { context.frameLayout { // we want (0, 0) at top left @@ -107,7 +136,8 @@ class PopupComponent : } private fun showKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { - val keys = PopupPreset[keyboard.label] + val keys = popupPresetJson?.get(keyboard.label) + ?: PopupPreset[keyboard.label] ?: EmojiModifier.produceSkinTones(keyboard.label) ?: return // clear popup preview text OR create empty popup preview From c77ca237f573f31d79ea18dd67f7cea6b8da3dcd Mon Sep 17 00:00:00 2001 From: fxliang Date: Fri, 20 Jun 2025 12:33:00 +0800 Subject: [PATCH 03/20] feat: layout of TextKeyboard by config/TextKeyboardLayout.json --- .../android/input/bar/ui/idle/NumberRow.kt | 2 +- .../expanded/ExpandedCandidateLayout.kt | 4 +- .../android/input/keyboard/BaseKeyboard.kt | 6 +- .../android/input/keyboard/KeyDefPreset.kt | 3 +- .../android/input/keyboard/NumberKeyboard.kt | 4 +- .../android/input/keyboard/TextKeyboard.kt | 79 ++++++++++++++++++- .../android/input/picker/PickerLayout.kt | 6 +- 7 files changed, 91 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt index 0da95932a..c4354a439 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt @@ -13,7 +13,7 @@ import org.fcitx.fcitx5.android.input.keyboard.KeyAction import org.fcitx.fcitx5.android.input.keyboard.KeyDef @SuppressLint("ViewConstructor") -class NumberRow(ctx: Context, theme: Theme) : BaseKeyboard(ctx, theme, Layout) { +class NumberRow(ctx: Context, theme: Theme) : BaseKeyboard(ctx, theme, ::Layout) { companion object { val Layout = listOf( listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map { digit -> diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt index a4151e09f..f1655b9e6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt @@ -31,7 +31,7 @@ import splitties.views.imageResource @SuppressLint("ViewConstructor") class ExpandedCandidateLayout(context: Context, theme: Theme) : ConstraintLayout(context) { - class Keyboard(context: Context, theme: Theme) : BaseKeyboard(context, theme, Layout) { + class Keyboard(context: Context, theme: Theme) : BaseKeyboard(context, theme, ::Layout) { companion object { const val UpBtnLabel = "U" const val DownBtnLabel = "D" @@ -111,4 +111,4 @@ class ExpandedCandidateLayout(context: Context, theme: Theme) : ConstraintLayout fun resetPosition() { recyclerView.scrollToPosition(0) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt index 84a21447c..973a9774e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt @@ -46,9 +46,11 @@ import kotlin.math.roundToInt abstract class BaseKeyboard( context: Context, protected val theme: Theme, - private val keyLayout: List> + private val layoutProvider: () ->List> ) : ConstraintLayout(context) { + private val keyLayout: List> + get() = layoutProvider() var keyActionListener: KeyActionListener? = null private val prefs = AppPrefs.getInstance() @@ -503,4 +505,4 @@ abstract class BaseKeyboard( // do nothing by default } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt index 96b1ba94b..c6d8ced64 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt @@ -42,11 +42,12 @@ class SymbolKey( class AlphabetKey( val character: String, val punctuation: String, + val displayText: String = character, variant: Variant = Variant.Normal, popup: Array? = null ) : KeyDef( Appearance.AltText( - displayText = character, + displayText = displayText, altText = punctuation, textSize = 23f, variant = variant diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt index 89dff000a..6c20335a0 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt @@ -16,7 +16,7 @@ import splitties.views.imageResource class NumberKeyboard( context: Context, theme: Theme, -) : BaseKeyboard(context, theme, Layout) { +) : BaseKeyboard(context, theme, ::Layout) { companion object { const val Name = "Number" @@ -68,4 +68,4 @@ class NumberKeyboard( // leave empty on purpose to disable popup in NumberKeyboard } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index f3476aafb..4038567df 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -18,19 +18,94 @@ import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.popup.PopupAction import splitties.views.imageResource +import java.io.File +import kotlinx.serialization.json.Json +import org.fcitx.fcitx5.android.utils.appContext +import kotlinx.serialization.Serializable @SuppressLint("ViewConstructor") class TextKeyboard( context: Context, theme: Theme -) : BaseKeyboard(context, theme, Layout) { +) : BaseKeyboard(context, theme, ::Layout) { enum class CapsState { None, Once, Lock } companion object { const val Name = "Text" + private var lastModified = 0L - val Layout: List> = listOf( + @Serializable + data class KeyJson( + val type: String, + val main: String? = null, + val alt: String? = null, + val displayText: String? = null, + val label: String? = null, + val subLabel: String? = null, + val weight: Float? = null + ) + var cachedLayoutJson: List>? = null + val textLayoutJson : List>? + @Synchronized + get() { + val file = File(appContext.getExternalFilesDir(null), "config/TextKeyboardLayout.json") + if (!file.exists()) { + cachedLayoutJson = null + return null + } + if (cachedLayoutJson == null || file.lastModified() != lastModified) { + try { + lastModified = file.lastModified() + val json = file.readText() + cachedLayoutJson = Json.decodeFromString>>(json) + } catch (e: Exception) { + e.printStackTrace() + cachedLayoutJson = null + } + } + return cachedLayoutJson + } + val Layout: List> + get() = textLayoutJson?.let { jsonLayout -> + try { + jsonLayout.map { row -> + row.map { keyJson -> + when (keyJson.type) { + "AlphabetKey" -> AlphabetKey( + keyJson.main ?: "", + keyJson.alt ?: "", + keyJson.displayText ?: keyJson.main ?: "" + ) + "CapsKey" -> CapsKey() + "BackspaceKey" -> BackspaceKey() + "LayoutSwitchKey" -> LayoutSwitchKey( + keyJson.label ?: "", + keyJson.subLabel ?: "" + ) + "CommaKey" -> CommaKey( + keyJson.weight ?: 1.0f, + KeyDef.Appearance.Variant.Alternative + ) + "LanguageKey" -> LanguageKey() + "SpaceKey" -> SpaceKey() + "SymbolKey" -> SymbolKey( + keyJson.label ?: "", + keyJson.weight ?: 1.0f, + KeyDef.Appearance.Variant.Alternative + ) + "ReturnKey" -> ReturnKey() + else -> throw IllegalArgumentException("Unknown key type: ${keyJson.type}") + } + } + } + } catch (e: Exception) { + e.printStackTrace() + defaultLayout + } + } ?: defaultLayout + + val defaultLayout: List> = listOf( listOf( AlphabetKey("Q", "1"), AlphabetKey("W", "2"), diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt index 248fbb13b..0998bfb24 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt @@ -28,7 +28,7 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : ConstraintLayout(context) { class Keyboard(context: Context, theme: Theme, switchKey: KeyDef) : BaseKeyboard( - context, theme, listOf( + context, theme, {listOf( listOf( LayoutSwitchKey("ABC", TextKeyboard.Name), PunctuationKey(","), @@ -37,7 +37,7 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : PunctuationKey("."), ReturnKey() ) - ) + )} ) { class PunctuationKey(val symbol: String) : KeyDef( @@ -84,4 +84,4 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : below(pager, dp(-1)) }) } -} \ No newline at end of file +} From 43340bca7ed9b982766631aa461f59c2ed76e332 Mon Sep 17 00:00:00 2001 From: fxliang Date: Thu, 26 Jun 2025 03:12:01 +0000 Subject: [PATCH 04/20] feat: font face of main text(AutoScaleTextView) and candiates fonts/fontset.json { cand_font: LXGWWenKai-Regular.ttf, font: segoepr.ttf } fonts/*.ttf or fonts/*.otf --- .../fcitx5/android/input/AutoScaleTextView.kt | 50 +++++++++++++++++++ .../input/candidates/CandidateItemUi.kt | 38 ++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index 0fb52735c..de600823c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -17,6 +17,10 @@ import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max import kotlin.math.min +import org.json.JSONObject +import java.io.File +import android.graphics.Typeface +import org.fcitx.fcitx5.android.utils.appContext @SuppressLint("AppCompatCustomView") class AutoScaleTextView @JvmOverloads constructor( @@ -53,6 +57,52 @@ class AutoScaleTextView @JvmOverloads constructor( private var textScaleX = 1.0f private var textScaleY = 1.0f + companion object { + private var cachedTypeface: Typeface? = null + private var cachedFontFilePath: String? = null + fun getFontTypeFace(key: String): Typeface? { + if (cachedTypeface != null) return cachedTypeface + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + val jsonFile = File(fontsDir, "fontset.json") + if (!jsonFile.exists()) return null + return try { + val json = JSONObject(jsonFile.readText()) + val fontName = if (json.has(key)) json.getString(key) else return null + val fontFile = File(fontsDir, fontName) + if (!fontFile.exists()) return null + if (cachedFontFilePath != fontFile.absolutePath) { + cachedTypeface = Typeface.createFromFile(fontFile) + cachedFontFilePath = fontFile.absolutePath + } + cachedTypeface + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + init { + getFontTypeFace("font")?.let { typeface -> + setTypeface(typeface) + } + } + + /* + init { + getCandidateFontFile()?.let { file -> + if (file.exists()) { + try { + typeface = Typeface.createFromFile(file) + setTypeface(typeface) + } catch (e: Exception) { + e.printStackTrace() + } + } else { + } + } + } + */ + override fun setText(charSequence: CharSequence?, bufferType: BufferType) { // setText can be called in super constructor if (!::text.isInitialized || charSequence == null || !text.contentEquals(charSequence)) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt index e927b15fb..2139afa80 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt @@ -17,6 +17,10 @@ import splitties.views.dsl.core.matchParent import splitties.views.dsl.core.view import splitties.views.dsl.core.wrapContent import splitties.views.gravityCenter +import org.json.JSONObject +import java.io.File +import android.graphics.Typeface +import org.fcitx.fcitx5.android.utils.appContext class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui { @@ -28,6 +32,40 @@ class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui { setTextColor(theme.candidateTextColor) } + //* + companion object { + private var cachedTypeface: Typeface? = null + private var cachedFontFilePath: String? = null + fun getFontTypeFace(key: String): Typeface? { + if (cachedTypeface != null) return cachedTypeface + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + val jsonFile = File(fontsDir, "fontset.json") + if (!jsonFile.exists()) return null + return try { + val json = JSONObject(jsonFile.readText()) + val fontName = if (json.has(key)) json.getString(key) else return null + val fontFile = File(fontsDir, fontName) + if (!fontFile.exists()) return null + if (cachedFontFilePath != fontFile.absolutePath) { + cachedTypeface = Typeface.createFromFile(fontFile) + cachedFontFilePath = fontFile.absolutePath + } + cachedTypeface + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + //*/ + + init { + // text.setFontTypeFace("cand_font") + getFontTypeFace("cand_font")?.let { typeface -> + text.typeface = typeface + } + } + override val root = view(::CustomGestureView) { background = pressHighlightDrawable(theme.keyPressHighlightColor) From cdf749f53720090eb211deb136c22c02566661c7 Mon Sep 17 00:00:00 2001 From: fxliang Date: Thu, 26 Jun 2025 15:49:57 +0800 Subject: [PATCH 05/20] refactor: cached all typefaces defined in json on startup; feat: add key_main_font and key_alt_font for keyview, when font for all AutoScaleTextView --- .../fcitx5/android/input/AutoScaleTextView.kt | 63 ++++++++++--------- .../input/candidates/CandidateItemUi.kt | 32 +--------- .../fcitx5/android/input/keyboard/KeyView.kt | 2 + 3 files changed, 36 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index de600823c..04bfc37f6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -58,50 +58,53 @@ class AutoScaleTextView @JvmOverloads constructor( private var textScaleY = 1.0f companion object { - private var cachedTypeface: Typeface? = null - private var cachedFontFilePath: String? = null - fun getFontTypeFace(key: String): Typeface? { - if (cachedTypeface != null) return cachedTypeface + private var cachedFontTypefaceMap: MutableMap = mutableMapOf() + private var fontTypefaceMapInitialized = false + + fun loadFontTypeFaces() { val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") val jsonFile = File(fontsDir, "fontset.json") - if (!jsonFile.exists()) return null - return try { + if (!jsonFile.exists()) { + cachedFontTypefaceMap.clear() + return + } + try { val json = JSONObject(jsonFile.readText()) - val fontName = if (json.has(key)) json.getString(key) else return null - val fontFile = File(fontsDir, fontName) - if (!fontFile.exists()) return null - if (cachedFontFilePath != fontFile.absolutePath) { - cachedTypeface = Typeface.createFromFile(fontFile) - cachedFontFilePath = fontFile.absolutePath + json.keys().forEach { key -> + val fontName = json.getString(key) + val fontFile = File(fontsDir, fontName) + val typeface = if (fontFile.exists()) { + try { + Typeface.createFromFile(fontFile) + } catch (e: Exception) { + e.printStackTrace() + null + } + } else null + cachedFontTypefaceMap[key] = typeface } - cachedTypeface } catch (e: Exception) { e.printStackTrace() - null + cachedFontTypefaceMap.clear() } + fontTypefaceMapInitialized = true } } - init { - getFontTypeFace("font")?.let { typeface -> - setTypeface(typeface) - } + + fun setFontTypeFace(key: String) { + cachedFontTypefaceMap[key]?.let { typeface -> + setTypeface(typeface) + } ?: run { + setTypeface(Typeface.DEFAULT) + } } - /* init { - getCandidateFontFile()?.let { file -> - if (file.exists()) { - try { - typeface = Typeface.createFromFile(file) - setTypeface(typeface) - } catch (e: Exception) { - e.printStackTrace() - } - } else { + if (!fontTypefaceMapInitialized) { + loadFontTypeFaces() } - } + setFontTypeFace("font") } - */ override fun setText(charSequence: CharSequence?, bufferType: BufferType) { // setText can be called in super constructor diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt index 2139afa80..506b0ff65 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt @@ -32,38 +32,8 @@ class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui { setTextColor(theme.candidateTextColor) } - //* - companion object { - private var cachedTypeface: Typeface? = null - private var cachedFontFilePath: String? = null - fun getFontTypeFace(key: String): Typeface? { - if (cachedTypeface != null) return cachedTypeface - val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") - val jsonFile = File(fontsDir, "fontset.json") - if (!jsonFile.exists()) return null - return try { - val json = JSONObject(jsonFile.readText()) - val fontName = if (json.has(key)) json.getString(key) else return null - val fontFile = File(fontsDir, fontName) - if (!fontFile.exists()) return null - if (cachedFontFilePath != fontFile.absolutePath) { - cachedTypeface = Typeface.createFromFile(fontFile) - cachedFontFilePath = fontFile.absolutePath - } - cachedTypeface - } catch (e: Exception) { - e.printStackTrace() - null - } - } - } - //*/ - init { - // text.setFontTypeFace("cand_font") - getFontTypeFace("cand_font")?.let { typeface -> - text.typeface = typeface - } + text.setFontTypeFace("cand_font") } override val root = view(::CustomGestureView) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt index 2252d7459..cc1002ed3 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt @@ -244,6 +244,7 @@ open class TextKeyView(ctx: Context, theme: Theme, def: KeyDef.Appearance.Text) setTextSize(TypedValue.COMPLEX_UNIT_DIP, def.textSize) textDirection = View.TEXT_DIRECTION_FIRST_STRONG_LTR // keep original typeface, apply textStyle only + setFontTypeFace("key_main_font") setTypeface(typeface, def.textStyle) setTextColor( when (def.variant) { @@ -271,6 +272,7 @@ class AltTextKeyView(ctx: Context, theme: Theme, def: KeyDef.Appearance.AltText) isFocusable = false // TODO hardcoded alt text size setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10.666667f) + setFontTypeFace("key_alt_font") setTypeface(typeface, Typeface.BOLD) text = def.altText textDirection = View.TEXT_DIRECTION_FIRST_STRONG_LTR From a24b8552c302f2d9a6b686dca52c2d59f7f5c967 Mon Sep 17 00:00:00 2001 From: fxliang Date: Thu, 26 Jun 2025 16:49:52 +0800 Subject: [PATCH 06/20] refactor: make fontset.json auto reload when file modified and reactivate f5a --- .../fcitx5/android/input/AutoScaleTextView.kt | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index 04bfc37f6..6a98b4096 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -58,51 +58,48 @@ class AutoScaleTextView @JvmOverloads constructor( private var textScaleY = 1.0f companion object { - private var cachedFontTypefaceMap: MutableMap = mutableMapOf() - private var fontTypefaceMapInitialized = false - - fun loadFontTypeFaces() { - val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") - val jsonFile = File(fontsDir, "fontset.json") - if (!jsonFile.exists()) { - cachedFontTypefaceMap.clear() - return - } - try { - val json = JSONObject(jsonFile.readText()) - json.keys().forEach { key -> - val fontName = json.getString(key) - val fontFile = File(fontsDir, fontName) - val typeface = if (fontFile.exists()) { - try { - Typeface.createFromFile(fontFile) - } catch (e: Exception) { - e.printStackTrace() - null + private var cachedFontTypefaceMap: MutableMap? = null + private var lastModified = 0L + val fontTypefaceMap: MutableMap + @Synchronized + get() { + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + val jsonFile = File(fontsDir, "fontset.json") + if (!jsonFile.exists()) { + cachedFontTypefaceMap = null + return mutableMapOf() // 返回空Map而非null + } + if (cachedFontTypefaceMap == null || lastModified != jsonFile.lastModified()) { + val newMap = mutableMapOf() + try { + val json = JSONObject(jsonFile.readText().replace(Regex("//.*?\\n"), "")) + json.keys().forEach { key -> + val fontName = json.getString(key) + val fontFile = File(fontsDir, fontName) + val typeface = if (fontFile.exists()) { + try { + Typeface.createFromFile(fontFile) + } catch (e: Exception) { + null + } + } else null + newMap[key] = typeface } - } else null - cachedFontTypefaceMap[key] = typeface + cachedFontTypefaceMap = newMap + lastModified = jsonFile.lastModified() + } catch (e: Exception) { + cachedFontTypefaceMap = newMap // 即使出错也返回新Map + } } - } catch (e: Exception) { - e.printStackTrace() - cachedFontTypefaceMap.clear() + return cachedFontTypefaceMap ?: mutableMapOf() } - fontTypefaceMapInitialized = true - } } fun setFontTypeFace(key: String) { - cachedFontTypefaceMap[key]?.let { typeface -> - setTypeface(typeface) - } ?: run { - setTypeface(Typeface.DEFAULT) - } + setTypeface(fontTypefaceMap[key] ?: Typeface.DEFAULT) } init { - if (!fontTypefaceMapInitialized) { - loadFontTypeFaces() - } setFontTypeFace("font") } From 8e743a80ce4971f7a5a1b3428fc13ab33663007f Mon Sep 17 00:00:00 2001 From: fxliang Date: Thu, 26 Jun 2025 17:14:26 +0800 Subject: [PATCH 07/20] feat: add popup_key_font for popup window --- .../java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt index f7ba9c4d3..9deea627c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt @@ -63,6 +63,7 @@ class PopupKeyboardUi( scaleMode = AutoScaleTextView.Mode.Proportional textSize = 23f setTextColor(theme.keyTextColor) + setFontTypeFace("popup_key_font") } override val root = frameLayout { From 10f5eb4c3f01897da57d052b57509caded3bed2d Mon Sep 17 00:00:00 2001 From: fxliang Date: Fri, 27 Jun 2025 11:18:22 +0800 Subject: [PATCH 08/20] feat: add preedit_font key for fontset.json --- .../java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt index 7c7a8b476..dc3b10b6d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt @@ -23,6 +23,7 @@ import splitties.views.dsl.core.add import splitties.views.dsl.core.lParams import splitties.views.dsl.core.textView import splitties.views.dsl.core.verticalLayout +import org.fcitx.fcitx5.android.input.AutoScaleTextView open class PreeditUi( override val ctx: Context, @@ -48,6 +49,7 @@ open class PreeditUi( setTextColor(theme.keyTextColor) textSize = 16f setupTextView?.invoke(this) + typeface = AutoScaleTextView.fontTypefaceMap["preedit_font"] ?: typeface } private val upView = createTextView() From 4b3a9689d61bb4f1f803fa2d68da45570eea55d7 Mon Sep 17 00:00:00 2001 From: fxliang Date: Fri, 27 Jun 2025 11:20:42 +0800 Subject: [PATCH 09/20] refactor: more kotlin style in update cachedFontTypefaceMap --- .../fcitx5/android/input/AutoScaleTextView.kt | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index 6a98b4096..03f39c334 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -70,26 +70,19 @@ class AutoScaleTextView @JvmOverloads constructor( return mutableMapOf() // 返回空Map而非null } if (cachedFontTypefaceMap == null || lastModified != jsonFile.lastModified()) { - val newMap = mutableMapOf() - try { - val json = JSONObject(jsonFile.readText().replace(Regex("//.*?\\n"), "")) - json.keys().forEach { key -> - val fontName = json.getString(key) - val fontFile = File(fontsDir, fontName) - val typeface = if (fontFile.exists()) { - try { - Typeface.createFromFile(fontFile) - } catch (e: Exception) { - null - } - } else null - newMap[key] = typeface - } - cachedFontTypefaceMap = newMap - lastModified = jsonFile.lastModified() - } catch (e: Exception) { - cachedFontTypefaceMap = newMap // 即使出错也返回新Map - } + cachedFontTypefaceMap = runCatching { + JSONObject(jsonFile.readText().replace(Regex("//.*?\\n"), "")) + .let { json -> + json.keys().asSequence().associateTo(mutableMapOf()) { key -> + key to runCatching { + File(fontsDir, json.getString(key)) + .takeIf { it.exists() } + ?.let { Typeface.createFromFile(it) } + }.getOrNull() + } + } as MutableMap // 确保返回可变Map + }.getOrElse { mutableMapOf() } + lastModified = jsonFile.lastModified() } return cachedFontTypefaceMap ?: mutableMapOf() } From 35a1e4e577af3e847c13c878f7e9cc4e61849b40 Mon Sep 17 00:00:00 2001 From: fxliang Date: Fri, 27 Jun 2025 12:34:07 +0800 Subject: [PATCH 10/20] refactor: TextKeyboardLayout with map by ime.uniqueName, im.subMode, and im.subMode.label --- .../fcitx5/android/input/keyboard/KeyDef.kt | 3 +- .../android/input/keyboard/KeyDefPreset.kt | 2 + .../android/input/keyboard/TextKeyboard.kt | 153 ++++++++++++++++-- 3 files changed, 140 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt index 211a2b37f..d0e9ecc53 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt @@ -48,6 +48,7 @@ open class KeyDef( class AltText( displayText: String, val altText: String, + val character: String, textSize: Float, /** * `Int` constants in [Typeface]. @@ -123,4 +124,4 @@ open class KeyDef( class Item(val label: String, @DrawableRes val icon: Int, val action: KeyAction) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt index c6d8ced64..e95b4c955 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt @@ -49,6 +49,7 @@ class AlphabetKey( Appearance.AltText( displayText = displayText, altText = punctuation, + character = character, textSize = 23f, variant = variant ), @@ -71,6 +72,7 @@ class AlphabetDigitKey( Appearance.AltText( displayText = character, altText = altText, + character = character, textSize = 23f ), setOf( diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index 4038567df..23932d3ae 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -19,10 +19,34 @@ import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.popup.PopupAction import splitties.views.imageResource import java.io.File -import kotlinx.serialization.json.Json +import kotlinx.serialization.json.* import org.fcitx.fcitx5.android.utils.appContext import kotlinx.serialization.Serializable +object DisplayTextResolver { + fun resolve( + displayText: JsonElement?, + subModeLabel: String, + default: String + ): String { + return when { + displayText == null -> default + displayText is JsonPrimitive -> displayText.content + displayText is JsonObject -> resolveMap(displayText, subModeLabel) ?: default + else -> default + } + } + + private fun resolveMap( + map: JsonObject, + subModeLabel: String + ): String? { + // 直接匹配子模式标签 + return map[subModeLabel]?.jsonPrimitive?.content + ?: map[""]?.jsonPrimitive?.content + } +} + @SuppressLint("ViewConstructor") class TextKeyboard( context: Context, @@ -34,40 +58,48 @@ class TextKeyboard( companion object { const val Name = "Text" private var lastModified = 0L + var ime: InputMethodEntry? = null @Serializable data class KeyJson( val type: String, val main: String? = null, val alt: String? = null, - val displayText: String? = null, + val displayText: JsonElement? = null, val label: String? = null, val subLabel: String? = null, val weight: Float? = null ) - var cachedLayoutJson: List>? = null - val textLayoutJson : List>? + var cachedLayoutJsonMap: Map>>? = null + + val textLayoutJsonMap: Map>>? @Synchronized get() { val file = File(appContext.getExternalFilesDir(null), "config/TextKeyboardLayout.json") if (!file.exists()) { - cachedLayoutJson = null + cachedLayoutJsonMap = null return null } - if (cachedLayoutJson == null || file.lastModified() != lastModified) { + if (cachedLayoutJsonMap == null || file.lastModified() != lastModified) { try { lastModified = file.lastModified() val json = file.readText() - cachedLayoutJson = Json.decodeFromString>>(json) + cachedLayoutJsonMap = Json.decodeFromString>>>(json) } catch (e: Exception) { e.printStackTrace() - cachedLayoutJson = null + cachedLayoutJsonMap = null } } - return cachedLayoutJson + return cachedLayoutJsonMap } + + private fun getTextLayoutJsonForIme(displayName: String): List>? { + val map = textLayoutJsonMap ?: return null + return map[displayName] ?: defaultLayoutJson + } + val Layout: List> - get() = textLayoutJson?.let { jsonLayout -> + get() = getTextLayoutJsonForIme(ime?.uniqueName ?: "default")?.let { jsonLayout -> try { jsonLayout.map { row -> row.map { keyJson -> @@ -75,7 +107,11 @@ class TextKeyboard( "AlphabetKey" -> AlphabetKey( keyJson.main ?: "", keyJson.alt ?: "", - keyJson.displayText ?: keyJson.main ?: "" + DisplayTextResolver.resolve( + keyJson.displayText, + ime?.subMode?.label ?: "", + keyJson.main ?: "" + ) ) "CapsKey" -> CapsKey() "BackspaceKey" -> BackspaceKey() @@ -149,6 +185,57 @@ class TextKeyboard( ReturnKey() ) ) + + val defaultLayoutJson: List> = Json.decodeFromString>>( + DEFAULT_KEYBOARD_LAYOUT_JSON + ) + + const val DEFAULT_KEYBOARD_LAYOUT_JSON = """ + [ + [ + {"type": "AlphabetKey", "main": "Q", "alt": "1"}, + {"type": "AlphabetKey", "main": "W", "alt": "2"}, + {"type": "AlphabetKey", "main": "E", "alt": "3"}, + {"type": "AlphabetKey", "main": "R", "alt": "4"}, + {"type": "AlphabetKey", "main": "T", "alt": "5"}, + {"type": "AlphabetKey", "main": "Y", "alt": "6"}, + {"type": "AlphabetKey", "main": "U", "alt": "7"}, + {"type": "AlphabetKey", "main": "I", "alt": "8"}, + {"type": "AlphabetKey", "main": "O", "alt": "9"}, + {"type": "AlphabetKey", "main": "P", "alt": "0"} + ], + [ + {"type": "AlphabetKey", "main": "A", "alt": "@"}, + {"type": "AlphabetKey", "main": "S", "alt": "*"}, + {"type": "AlphabetKey", "main": "D", "alt": "+"}, + {"type": "AlphabetKey", "main": "F", "alt": "-"}, + {"type": "AlphabetKey", "main": "G", "alt": "="}, + {"type": "AlphabetKey", "main": "H", "alt": "/"}, + {"type": "AlphabetKey", "main": "J", "alt": "#"}, + {"type": "AlphabetKey", "main": "K", "alt": "("}, + {"type": "AlphabetKey", "main": "L", "alt": ")"} + ], + [ + {"type": "CapsKey"}, + {"type": "AlphabetKey", "main": "Z", "alt": "'"}, + {"type": "AlphabetKey", "main": "X", "alt": ":"}, + {"type": "AlphabetKey", "main": "C", "alt": "\""}, + {"type": "AlphabetKey", "main": "V", "alt": "?"}, + {"type": "AlphabetKey", "main": "B", "alt": "!"}, + {"type": "AlphabetKey", "main": "N", "alt": "~"}, + {"type": "AlphabetKey", "main": "M", "alt": "\\"}, + {"type": "BackspaceKey"} + ], + [ + {"type": "LayoutSwitchKey", "label": "?123", "subLabel": ""}, + {"type": "CommaKey", "weight": 0.1}, + {"type": "LanguageKey"}, + {"type": "SpaceKey"}, + {"type": "SymbolKey", "label": ".", "weight": 0.1}, + {"type": "ReturnKey"} + ] + ] + """ } val caps: ImageKeyView by lazy { findViewById(R.id.button_caps) } @@ -240,6 +327,9 @@ class TextKeyboard( } override fun onInputMethodUpdate(ime: InputMethodEntry) { + // update ime of companion object ime + TextKeyboard.ime = ime + updateAlphabetKeys() space.mainText.text = buildString { append(ime.displayName) ime.subMode.run { label.ifEmpty { name.ifEmpty { null } } }?.let { append(" ($it)") } @@ -302,11 +392,40 @@ class TextKeyboard( } private fun updateAlphabetKeys() { - textKeys.forEach { - if (it.def !is KeyDef.Appearance.AltText) return - it.mainText.text = it.def.displayText.let { str -> - if (str.length != 1 || !str[0].isLetter()) return@forEach - if (keepLettersUppercase) str.uppercase() else transformAlphabet(str) + val layoutJson = getTextLayoutJsonForIme(ime?.uniqueName ?: "default") + if (layoutJson != null) { + textKeys.forEach { + if (it.def !is KeyDef.Appearance.AltText) return@forEach + val keyJson = layoutJson.flatten().find { key -> key.main == it.def.character } + val displayText = if (keyJson != null ) { + DisplayTextResolver.resolve( + keyJson.displayText, + ime?.subMode?.label ?: "", + keyJson.main ?: "" + ) + } else { + it.def.character + } + // val displayText = keyJson?.displayText ?: keyJson?.main ?: it.def.character + + it.mainText.text = displayText?.let { str -> + if (keepLettersUppercase) { + keyJson?.main?.uppercase() ?: str.uppercase() + } else { + when(capsState) { + CapsState.None -> displayText.lowercase() ?: keyJson?.main?.lowercase() ?: str.lowercase() + else -> keyJson?.main?.uppercase() ?: str.uppercase() + } + } + } ?: it.def.character + } + } else { + textKeys.forEach { + if (it.def !is KeyDef.Appearance.AltText) return + it.mainText.text = it.def.displayText.let { str -> + if (str.length != 1 || !str[0].isLetter()) return@forEach + if (keepLettersUppercase) str.uppercase() else transformAlphabet(str) + } } } } @@ -326,4 +445,4 @@ class TextKeyboard( } } -} \ No newline at end of file +} From 96d7a7c01c642dc4ca1dc430293519a4f449888f Mon Sep 17 00:00:00 2001 From: fxliang Date: Mon, 30 Jun 2025 15:03:45 +0800 Subject: [PATCH 11/20] chore: translation for Emoji skin tone selection and preference --- app/src/main/res/values-zh-rCN/strings.xml | 8 ++++++++ app/src/main/res/values-zh-rTW/strings.xml | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4f9dfbcd3..87b7ebd6e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -303,4 +303,12 @@ 把光标移动到结尾 打开输入法设置 全部删除 + 表情和符号 + 默认表情符号肤色修饰符 + 无修饰符 + 🏻 非常浅色 + 🏼 浅色 + 🏽 中等色 + 🏾 深色 + 🏿 非常深色 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index eb8a1dce8..fb474fe6a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -284,4 +284,12 @@ 忽略系統窗口邊襯區 (WindowInsets) 瀏覽用户資料目錄 重複按鍵時的觸覺回饋 - + 表情和符號 + 預設表情符號膚色修飾符 + 無修飾符 + 🏻 非常淺色 + 🏼 淺色 + 🏽 中等色 + 🏾 深色 + 🏿 非常深色 + From 7063622573912adaa238d35e648ce256f801e967 Mon Sep 17 00:00:00 2001 From: fxliang Date: Tue, 1 Jul 2025 09:32:05 +0800 Subject: [PATCH 12/20] fix: NullPointerException when TextKeyboard init, lazy view might be not ready --- .../fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index 23932d3ae..44015be44 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -255,8 +255,6 @@ class TextKeyboard( private val keepLettersUppercase by AppPrefs.getInstance().keyboard.keepLettersUppercase init { - updateLangSwitchKey(showLangSwitchKey.getValue()) - showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener) } private val textKeys: List by lazy { @@ -317,6 +315,12 @@ class TextKeyboard( updateAlphabetKeys() } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + updateLangSwitchKey(showLangSwitchKey.getValue()) + showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener) + } + override fun onReturnDrawableUpdate(returnDrawable: Int) { `return`.img.imageResource = returnDrawable } From 7f31ce78198e648c599516d2366e2b06de741dc2 Mon Sep 17 00:00:00 2001 From: fxliang Date: Sun, 20 Jul 2025 19:30:32 +0800 Subject: [PATCH 13/20] refactor: keep displayText only to avoid crash. --- .../android/input/keyboard/TextKeyboard.kt | 98 +------------------ 1 file changed, 2 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index 44015be44..ecaa912b8 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -95,53 +95,10 @@ class TextKeyboard( private fun getTextLayoutJsonForIme(displayName: String): List>? { val map = textLayoutJsonMap ?: return null - return map[displayName] ?: defaultLayoutJson + return map[displayName] ?: null } - val Layout: List> - get() = getTextLayoutJsonForIme(ime?.uniqueName ?: "default")?.let { jsonLayout -> - try { - jsonLayout.map { row -> - row.map { keyJson -> - when (keyJson.type) { - "AlphabetKey" -> AlphabetKey( - keyJson.main ?: "", - keyJson.alt ?: "", - DisplayTextResolver.resolve( - keyJson.displayText, - ime?.subMode?.label ?: "", - keyJson.main ?: "" - ) - ) - "CapsKey" -> CapsKey() - "BackspaceKey" -> BackspaceKey() - "LayoutSwitchKey" -> LayoutSwitchKey( - keyJson.label ?: "", - keyJson.subLabel ?: "" - ) - "CommaKey" -> CommaKey( - keyJson.weight ?: 1.0f, - KeyDef.Appearance.Variant.Alternative - ) - "LanguageKey" -> LanguageKey() - "SpaceKey" -> SpaceKey() - "SymbolKey" -> SymbolKey( - keyJson.label ?: "", - keyJson.weight ?: 1.0f, - KeyDef.Appearance.Variant.Alternative - ) - "ReturnKey" -> ReturnKey() - else -> throw IllegalArgumentException("Unknown key type: ${keyJson.type}") - } - } - } - } catch (e: Exception) { - e.printStackTrace() - defaultLayout - } - } ?: defaultLayout - - val defaultLayout: List> = listOf( + val Layout: List> = listOf( listOf( AlphabetKey("Q", "1"), AlphabetKey("W", "2"), @@ -185,57 +142,6 @@ class TextKeyboard( ReturnKey() ) ) - - val defaultLayoutJson: List> = Json.decodeFromString>>( - DEFAULT_KEYBOARD_LAYOUT_JSON - ) - - const val DEFAULT_KEYBOARD_LAYOUT_JSON = """ - [ - [ - {"type": "AlphabetKey", "main": "Q", "alt": "1"}, - {"type": "AlphabetKey", "main": "W", "alt": "2"}, - {"type": "AlphabetKey", "main": "E", "alt": "3"}, - {"type": "AlphabetKey", "main": "R", "alt": "4"}, - {"type": "AlphabetKey", "main": "T", "alt": "5"}, - {"type": "AlphabetKey", "main": "Y", "alt": "6"}, - {"type": "AlphabetKey", "main": "U", "alt": "7"}, - {"type": "AlphabetKey", "main": "I", "alt": "8"}, - {"type": "AlphabetKey", "main": "O", "alt": "9"}, - {"type": "AlphabetKey", "main": "P", "alt": "0"} - ], - [ - {"type": "AlphabetKey", "main": "A", "alt": "@"}, - {"type": "AlphabetKey", "main": "S", "alt": "*"}, - {"type": "AlphabetKey", "main": "D", "alt": "+"}, - {"type": "AlphabetKey", "main": "F", "alt": "-"}, - {"type": "AlphabetKey", "main": "G", "alt": "="}, - {"type": "AlphabetKey", "main": "H", "alt": "/"}, - {"type": "AlphabetKey", "main": "J", "alt": "#"}, - {"type": "AlphabetKey", "main": "K", "alt": "("}, - {"type": "AlphabetKey", "main": "L", "alt": ")"} - ], - [ - {"type": "CapsKey"}, - {"type": "AlphabetKey", "main": "Z", "alt": "'"}, - {"type": "AlphabetKey", "main": "X", "alt": ":"}, - {"type": "AlphabetKey", "main": "C", "alt": "\""}, - {"type": "AlphabetKey", "main": "V", "alt": "?"}, - {"type": "AlphabetKey", "main": "B", "alt": "!"}, - {"type": "AlphabetKey", "main": "N", "alt": "~"}, - {"type": "AlphabetKey", "main": "M", "alt": "\\"}, - {"type": "BackspaceKey"} - ], - [ - {"type": "LayoutSwitchKey", "label": "?123", "subLabel": ""}, - {"type": "CommaKey", "weight": 0.1}, - {"type": "LanguageKey"}, - {"type": "SpaceKey"}, - {"type": "SymbolKey", "label": ".", "weight": 0.1}, - {"type": "ReturnKey"} - ] - ] - """ } val caps: ImageKeyView by lazy { findViewById(R.id.button_caps) } From 72518019a61a4a88a83c6ad5f42fcbf77aada133 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:39:57 +0800 Subject: [PATCH 14/20] feat: Add Ctrl+key combinations to popup keyboard This commit introduces the ability to send Ctrl+key combinations from the popup keyboard. - `PopupKeyboardUi` now handles keys prefixed with "Ctrl+". - It parses the key string to extract the actual character. - It attempts to map the character to a `KeyEvent.KEYCODE` and uses `service.sendCombinationKeyEvents` to send the Ctrl modified key event. - If mapping to `KEYCODE` fails, it falls back to sending an `FcitxKeyAction` with the `Ctrl` state. - The `FcitxInputMethodService` instance is now passed to `PopupKeyboardUi` to enable sending combination key events. - `PopupPreset` has been updated to include "Ctrl+[Key]" options for all Latin characters (both lowercase and uppercase) in their respective popup arrays. --- .../android/input/popup/PopupComponent.kt | 1 + .../android/input/popup/PopupKeyboardUi.kt | 54 ++++++++- .../fcitx5/android/input/popup/PopupPreset.kt | 104 +++++++++--------- 3 files changed, 104 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt index fe57c98f0..45b6b6e9e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt @@ -160,6 +160,7 @@ class PopupComponent : popupKeyHeight, // position popup keyboard higher, because of [^1] popupHeight + keyBottomMargin, + this.service, keys, labels ) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt index 9deea627c..553d878c1 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt @@ -7,9 +7,13 @@ package org.fcitx.fcitx5.android.input.popup import android.content.Context import android.graphics.Rect import android.graphics.drawable.GradientDrawable +import android.view.KeyEvent // Added import import android.view.ViewOutlineProvider +import org.fcitx.fcitx5.android.core.KeyState +import org.fcitx.fcitx5.android.core.KeyStates import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.AutoScaleTextView +import org.fcitx.fcitx5.android.input.FcitxInputMethodService // Added import import org.fcitx.fcitx5.android.input.keyboard.KeyAction import splitties.dimensions.dp import splitties.views.dsl.core.Ui @@ -39,6 +43,7 @@ import kotlin.math.roundToInt * @param keyHeight key height in popup keyboard * @param popupHeight popup preview view height. Used to transform gesture coordinate from * trigger view to popup keyboard view. See [offsetX] and [offsetY]. + * @param service The FcitxInputMethodService instance * @param keys character to commit when triggered * @param labels symbols to show on keys */ @@ -52,6 +57,7 @@ class PopupKeyboardUi( private val keyWidth: Int, private val keyHeight: Int, private val popupHeight: Int, + private val service: FcitxInputMethodService, // Added service parameter private val keys: Array, private val labels: Array ) : PopupContainerUi(ctx, theme, outerBounds, triggerBounds, onDismissSelf) { @@ -219,8 +225,50 @@ class PopupKeyboardUi( } override fun onTrigger(): KeyAction? { - val key = keys.getOrNull(focusedIndex) ?: return null - return KeyAction.FcitxKeyAction(key) - } + val rawKeyString = keys.getOrNull(focusedIndex) ?: return null + + if (rawKeyString.startsWith("Ctrl+") && rawKeyString.length > 5) { + val actualCharStr = rawKeyString.substring(5) + if (actualCharStr.isNotEmpty()) { + val charToPress = actualCharStr[0] + var keyCode = 0 + val upperChar = charToPress.uppercaseChar() + + // Simplified conversion: A-Z, 0-9 + if (upperChar in 'A'..'Z') { + keyCode = KeyEvent.KEYCODE_A + (upperChar.code - 'A'.code) + } else if (charToPress in '0'..'9') { + keyCode = KeyEvent.KEYCODE_0 + (charToPress.code - '0'.code) + } else { + // For other characters, this mapping is incomplete. + // You might want to add specific mappings for common symbols: + // when (charToPress) { + // '.' -> keyCode = KeyEvent.KEYCODE_PERIOD + // ',' -> keyCode = KeyEvent.KEYCODE_COMMA + // // ... etc. + // } + // android.util.Log.w("PopupKeyboardUi", "Cannot map char '$charToPress' to KeyEvent.KEYCODE for Ctrl operation.") + } + if (keyCode != 0) { + service.sendCombinationKeyEvents(keyCode, ctrl = true) + return null // Action handled directly by service, no further KeyAction needed + } else { + // Fallback: If keyCode couldn't be determined for sendCombinationKeyEvents, + // send the original FcitxKeyAction with Ctrl state. + // This ensures some Ctrl functionality remains if mapping fails. + return KeyAction.FcitxKeyAction( + act = actualCharStr, + states = KeyStates(KeyState.Ctrl, KeyState.Virtual) + ) + } + } else { + // "Ctrl+" but no character after it - unlikely, but handle defensively by sending as is. + return KeyAction.FcitxKeyAction(rawKeyString) + } + } else { + // Not a "Ctrl+" key, send as a normal character. + return KeyAction.FcitxKeyAction(rawKeyString) + } + } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt index 11905d2f1..47baa33f5 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt @@ -12,61 +12,61 @@ val PopupPreset: Map> = hashMapOf( // // Latin // - "q" to arrayOf("1", "Q"), - "w" to arrayOf("2", "W"), - "e" to arrayOf("3", "E", "ê", "ë", "ē", "é", "ě", "è", "ė", "ę", "ȩ", "ḝ", "ə"), - "r" to arrayOf("4", "R"), - "t" to arrayOf("5", "T"), - "y" to arrayOf("6", "Y", "ÿ", "ұ", "ү", "ӯ", "ў"), - "u" to arrayOf("7", "U", "û", "ü", "ū", "ú", "ǔ", "ù"), - "i" to arrayOf("8", "I", "î", "ï", "ī", "í", "ǐ", "ì", "į", "ı"), - "o" to arrayOf("9", "O", "ô", "ö", "ō", "ó", "ǒ", "ò", "œ", "ø", "õ"), - "p" to arrayOf("0", "P"), - "a" to arrayOf("@", "A", "â", "ä", "ā", "á", "ǎ", "à", "æ", "ã", "å"), - "s" to arrayOf("*", "S", "ß", "ś", "š", "ş"), - "d" to arrayOf("+", "D", "ð"), - "f" to arrayOf("-", "F"), - "g" to arrayOf("=", "G", "ğ"), - "h" to arrayOf("/", "H"), - "j" to arrayOf("#", "J"), - "k" to arrayOf("(", "[", "{", "K"), - "l" to arrayOf(")", "]", "}", "L", "ł"), - "z" to arrayOf("'", "Z", "`", "ž", "ź", "ż"), - "x" to arrayOf(":", "X", "×"), - "c" to arrayOf("\"", "C", "ç", "ć", "č"), - "v" to arrayOf("?", "V", "¿", "ü", "ǖ", "ǘ", "ǚ", "ǜ"), - "b" to arrayOf("!", "B", "¡"), - "n" to arrayOf("~", "N", "ñ", "ń"), - "m" to arrayOf("\\", "M"), + "q" to arrayOf("1", "Q", "Ctrl+q"), + "w" to arrayOf("2", "W", "Ctrl+w"), + "e" to arrayOf("3", "E", "ê", "ë", "ē", "é", "ě", "è", "ė", "ę", "ȩ", "ḝ", "ə", "Ctrl+e"), + "r" to arrayOf("4", "R", "Ctrl+r"), + "t" to arrayOf("5", "T", "Ctrl+t"), + "y" to arrayOf("6", "Y", "ÿ", "ұ", "ү", "ӯ", "ў", "Ctrl+y"), + "u" to arrayOf("7", "U", "û", "ü", "ū", "ú", "ǔ", "ù", "Ctrl+u"), + "i" to arrayOf("8", "I", "î", "ï", "ī", "í", "ǐ", "ì", "į", "ı", "Ctrl+i"), + "o" to arrayOf("9", "O", "ô", "ö", "ō", "ó", "ǒ", "ò", "œ", "ø", "õ", "Ctrl+o"), + "p" to arrayOf("0", "P", "Ctrl+p"), + "a" to arrayOf("@", "A", "â", "ä", "ā", "á", "ǎ", "à", "æ", "ã", "å", "Ctrl+a"), + "s" to arrayOf("*", "S", "ß", "ś", "š", "ş", "Ctrl+s"), + "d" to arrayOf("+", "D", "ð", "Ctrl+d"), + "f" to arrayOf("-", "F", "Ctrl+f"), + "g" to arrayOf("=", "G", "ğ", "Ctrl+g"), + "h" to arrayOf("/", "H", "Ctrl+h"), + "j" to arrayOf("#", "J", "Ctrl+j"), + "k" to arrayOf("(", "[", "{", "K", "Ctrl+k"), + "l" to arrayOf(")", "]", "}", "L", "ł", "Ctrl+l"), + "z" to arrayOf("'", "Z", "`", "ž", "ź", "ż", "Ctrl+z"), + "x" to arrayOf(":", "X", "×", "Ctrl+x"), + "c" to arrayOf("\"", "C", "ç", "ć", "č", "Ctrl+c"), + "v" to arrayOf("?", "V", "¿", "ü", "ǖ", "ǘ", "ǚ", "ǜ", "Ctrl+v"), + "b" to arrayOf("!", "B", "¡", "Ctrl+b"), + "n" to arrayOf("~", "N", "ñ", "ń", "Ctrl+n"), + "m" to arrayOf("\\", "M", "Ctrl+m"), // // Upper case Latin // - "Q" to arrayOf("1", "q"), - "W" to arrayOf("2", "w"), - "E" to arrayOf("3", "e", "Ê", "Ë", "Ē", "É", "È", "Ė", "Ę", "Ȩ", "Ḝ", "Ə"), - "R" to arrayOf("4", "r"), - "T" to arrayOf("5", "t"), - "Y" to arrayOf("6", "y", "Ÿ", "Ұ", "Ү", "Ӯ", "Ў"), - "U" to arrayOf("7", "u", "Û", "Ü", "Ù", "Ú", "Ū"), - "I" to arrayOf("8", "i", "Î", "Ï", "Í", "Ī", "Į", "Ì"), - "O" to arrayOf("9", "o", "Ô", "Ö", "Ò", "Ó", "Œ", "Ø", "Ō", "Õ"), - "P" to arrayOf("0", "p"), - "A" to arrayOf("@", "a", "Â", "Ä", "Ā", "Á", "À", "Æ", "Ã", "Å"), - "S" to arrayOf("*", "s", "ẞ", "Ś", "Š", "Ş"), - "D" to arrayOf("+", "d", "Ð"), - "F" to arrayOf("-", "f"), - "G" to arrayOf("=", "g", "Ğ"), - "H" to arrayOf("/", "h"), - "J" to arrayOf("#", "j"), - "K" to arrayOf("(", "k"), - "L" to arrayOf(")", "l", "Ł"), - "Z" to arrayOf("'", "z", "`", "Ž", "Ź", "Ż"), - "X" to arrayOf(":", "x"), - "C" to arrayOf("\"", "c", "Ç", "Ć", "Č"), - "V" to arrayOf("?", "v"), - "B" to arrayOf("!", "b", "¡"), - "N" to arrayOf("~", "n", "Ñ", "Ń"), - "M" to arrayOf("\\", "m"), + "Q" to arrayOf("1", "q", "Ctrl+Q"), + "W" to arrayOf("2", "w", "Ctrl+W"), + "E" to arrayOf("3", "e", "Ê", "Ë", "Ē", "É", "È", "Ė", "Ę", "Ȩ", "Ḝ", "Ə", "Ctrl+E"), + "R" to arrayOf("4", "r", "Ctrl+R"), + "T" to arrayOf("5", "t", "Ctrl+T"), + "Y" to arrayOf("6", "y", "Ÿ", "Ұ", "Ү", "Ӯ", "Ў", "Ctrl+Y"), + "U" to arrayOf("7", "u", "Û", "Ü", "Ù", "Ú", "Ū", "Ctrl+U"), + "I" to arrayOf("8", "i", "Î", "Ï", "Í", "Ī", "Į", "Ì", "Ctrl+I"), + "O" to arrayOf("9", "o", "Ô", "Ö", "Ò", "Ó", "Œ", "Ø", "Ō", "Õ", "Ctrl+O"), + "P" to arrayOf("0", "p", "Ctrl+P"), + "A" to arrayOf("@", "a", "Â", "Ä", "Ā", "Á", "À", "Æ", "Ã", "Å", "Ctrl+A"), + "S" to arrayOf("*", "s", "ẞ", "Ś", "Š", "Ş", "Ctrl+S"), + "D" to arrayOf("+", "d", "Ð", "Ctrl+D"), + "F" to arrayOf("-", "f", "Ctrl+F"), + "G" to arrayOf("=", "g", "Ğ", "Ctrl+G"), + "H" to arrayOf("/", "h", "Ctrl+H"), + "J" to arrayOf("#", "j", "Ctrl+J"), + "K" to arrayOf("(", "k", "Ctrl+K"), + "L" to arrayOf(")", "l", "Ł", "Ctrl+L"), + "Z" to arrayOf("'", "z", "`", "Ž", "Ź", "Ż", "Ctrl+Z"), + "X" to arrayOf(":", "x", "Ctrl+X"), + "C" to arrayOf("\"", "c", "Ç", "Ć", "Č", "Ctrl+C"), + "V" to arrayOf("?", "v", "Ctrl+V"), + "B" to arrayOf("!", "b", "¡", "Ctrl+B"), + "N" to arrayOf("~", "n", "Ñ", "Ń", "Ctrl+N"), + "M" to arrayOf("\\", "m", "Ctrl+M"), // // Upper case Cyrillic // From 395fbd9404e15f2fbfb0d8aa1a47263070254f14 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:45:48 +0800 Subject: [PATCH 15/20] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E7=9A=84=20tag=20=E8=BF=9B=E8=A1=8C=20release?= =?UTF-8?q?=EF=BC=8C=E9=98=B2=E6=AD=A2=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27a1b59c3..d8ec7138d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,7 @@ jobs: uses: 'marvinpinto/action-automatic-releases@latest' with: repo_token: ${{ secrets.GITHUB_TOKEN }} - automatic_release_tag: latest + automatic_release_tag: ${{ github.ref_name }} prerelease: true title: "Nightly Build" files: | From 3037bfe67ae81b7e6239f64c5ad315177f6185be Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:27:00 +0000 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E7=94=A8=E6=88=B7=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=92=8C=E9=85=8D=E7=BD=AE=E4=B8=8D=E5=90=8C=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E5=AD=97=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FONT_FEATURE_README.md | 132 ++++++++++++++++++ .../android/data/fonts/FontConfigTest.kt | 65 +++++++++ .../fcitx5/android/data/fonts/FontManager.kt | 113 +++++++++++++++ .../fcitx5/android/data/prefs/AppPrefs.kt | 30 ++++ .../fcitx5/android/data/prefs/FontPrefs.kt | 82 +++++++++++ .../fcitx5/android/input/AutoScaleTextView.kt | 20 ++- .../fcitx5/android/ui/main/MainFragment.kt | 5 + .../android/ui/main/settings/SettingsRoute.kt | 7 + .../behavior/FontsSettingsFragment.kt | 77 ++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 9 ++ app/src/main/res/values/strings.xml | 9 ++ 11 files changed, 546 insertions(+), 3 deletions(-) create mode 100644 FONT_FEATURE_README.md create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt diff --git a/FONT_FEATURE_README.md b/FONT_FEATURE_README.md new file mode 100644 index 000000000..36a492461 --- /dev/null +++ b/FONT_FEATURE_README.md @@ -0,0 +1,132 @@ +# Fcitx5 Android 字体配置功能实现 + +## 概述 + +本功能为 Fcitx5-android 项目添加了完整的字体配置管理功能,允许用户在设置界面中直接选择和配置不同组件的字体。 + +## 功能特性 + +### 支持的字体配置项 +- **候选字体** (`cand_font`): 候选词显示字体 +- **编码字体** (`preedit_font`): 输入编码显示字体 +- **弹出按键字体** (`popup_key_font`): 弹出键盘按键字体 +- **按键主字体** (`key_main_font`): 主要按键文字字体 +- **按键次字体** (`key_alt_font`): 按键辅助文字字体 +- **默认字体** (`font`): AutoScaleTextView 默认字体 + +### 核心功能 +1. **字体文件管理**: 自动扫描 `fonts/` 目录下的 `.ttf` 和 `.otf` 字体文件 +2. **实时配置**: 配置更改后立即生效,无需重启应用 +3. **配置持久化**: 自动保存配置到 `fonts/fontset.json` 文件 +4. **系统默认支持**: 可选择使用系统默认字体 + +## 实现细节 + +### 文件结构 + +``` +app/src/main/java/org/fcitx/fcitx5/android/ +├── data/ +│ ├── fonts/ +│ │ ├── FontManager.kt # 字体管理核心类 +│ │ └── FontConfigTest.kt # 测试工具类 +│ └── prefs/ +│ └── AppPrefs.kt # 添加了 Fonts 配置类 +└── ui/main/settings/behavior/ + └── FontsSettingsFragment.kt # 字体设置界面 +``` + +### 核心类说明 + +#### FontManager +- 管理字体文件扫描和字体选择对话框 +- 处理字体配置的读写和更新 +- 提供字体缓存清理功能 + +#### AppPrefs.Fonts +- 管理字体配置的 SharedPreferences 存储 +- 监听配置更改并触发自动更新 + +#### FontsSettingsFragment +- 提供用户友好的字体选择界面 +- 显示当前选择的字体名称 +- 集成字体选择对话框 + +### 配置文件格式 + +字体配置保存在 `fonts/fontset.json` 文件中: + +```json +{ + "cand_font": "LXGWWenKai-Regular.ttf", + "font": "", + "preedit_font": "segoepr.ttf", + "popup_key_font": "segoepr.ttf", + "key_main_font": "segoepr.ttf", + "key_alt_font": "segoepr.ttf" +} +``` + +空字符串表示使用系统默认字体。 + +## 使用方法 + +### 用户操作 +1. 将字体文件 (`.ttf` 或 `.otf`) 放入应用的 `fonts/` 目录 +2. 打开 Fcitx5 设置 → 字体 +3. 点击要配置的字体项目 +4. 从弹出的列表中选择字体 +5. 配置立即生效 + +### 开发者集成 +```kotlin +// 手动更新字体配置 +FontManager.updateFontConfiguration() + +// 获取可用字体列表 +val fonts = FontManager.getAvailableFonts() + +// 显示字体选择对话框 +FontManager.showFontPickerDialog(context, R.string.font_title) { selectedFont -> + // 处理选择的字体 +} +``` + +## 技术实现亮点 + +1. **模块化设计**: 字体管理功能独立模块,易于维护和扩展 +2. **异常安全**: 完善的错误处理,避免因字体配置问题导致应用崩溃 +3. **性能优化**: 字体缓存机制,避免重复加载 +4. **用户体验**: 实时预览字体名称,直观的选择界面 +5. **向后兼容**: 兼容现有的字体配置文件格式 + +## 国际化支持 + +已添加中英文字符串资源: +- 英文: `values/strings.xml` +- 简体中文: `values-zh-rCN/strings.xml` + +## 测试 + +使用 `FontConfigTest` 类进行功能验证: +```kotlin +// 创建测试配置 +FontConfigTest.createTestFontConfig() + +// 验证配置正确性 +val isValid = FontConfigTest.validateFontConfig() +``` + +## 注意事项 + +1. 字体文件需要放在应用的外部存储 `fonts/` 目录下 +2. 支持的字体格式:`.ttf`, `.otf` +3. 配置更改后会自动清理字体缓存以确保更改生效 +4. 空字体文件名表示使用系统默认字体 + +## 未来扩展 + +- 字体预览功能 +- 字体大小配置 +- 字体样式配置(粗体、斜体等) +- 字体文件在线下载功能 diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt new file mode 100644 index 000000000..7114ff722 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.data.fonts + +import org.fcitx.fcitx5.android.utils.appContext +import org.json.JSONObject +import java.io.File + +/** + * 字体配置测试工具类 + */ +object FontConfigTest { + + /** + * 创建一个测试字体配置 + */ + fun createTestFontConfig() { + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + if (!fontsDir.exists()) { + fontsDir.mkdirs() + } + + val configFile = File(fontsDir, "fontset.json") + val testConfig = JSONObject().apply { + put("cand_font", "") // 使用系统默认 + put("font", "") // 使用系统默认 + put("preedit_font", "") + put("popup_key_font", "") + put("key_main_font", "") + put("key_alt_font", "") + } + + configFile.writeText(testConfig.toString(2)) + println("创建测试字体配置: ${configFile.absolutePath}") + } + + /** + * 验证字体配置是否正确加载 + */ + fun validateFontConfig(): Boolean { + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + val configFile = File(fontsDir, "fontset.json") + + return try { + if (!configFile.exists()) { + println("字体配置文件不存在") + return false + } + + val config = JSONObject(configFile.readText()) + val requiredKeys = listOf("cand_font", "font", "preedit_font", "popup_key_font", "key_main_font", "key_alt_font") + + requiredKeys.all { key -> + config.has(key).also { hasKey -> + if (!hasKey) println("缺少必需的键: $key") + } + } + } catch (e: Exception) { + println("验证字体配置时出错: ${e.message}") + false + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt new file mode 100644 index 000000000..fcd82a0a9 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.data.fonts + +import android.app.AlertDialog +import android.content.Context +import androidx.annotation.StringRes +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.prefs.AppPrefs +import org.fcitx.fcitx5.android.input.AutoScaleTextView +import org.fcitx.fcitx5.android.utils.appContext +import org.json.JSONObject +import java.io.File + +object FontManager { + + private val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + private val fontConfigFile = File(fontsDir, "fontset.json") + + init { + if (!fontsDir.exists()) { + fontsDir.mkdirs() + } + } + + /** + * 获取可用字体列表 + */ + fun getAvailableFonts(): List { + val fonts = mutableListOf() + + // 添加系统默认字体 + fonts.add(FontInfo("", "System Default")) + + // 扫描 fonts 目录 + if (fontsDir.exists()) { + fontsDir.listFiles { file -> + file.isFile && (file.extension.lowercase() in listOf("ttf", "otf")) + }?.forEach { file -> + fonts.add(FontInfo(file.name, file.nameWithoutExtension)) + } + } + + return fonts.sortedBy { it.displayName } + } + + /** + * 显示字体选择对话框 + */ + fun showFontPickerDialog( + context: Context, + @StringRes titleRes: Int, + onFontSelected: (String) -> Unit + ) { + val fonts = getAvailableFonts() + val fontNames = fonts.map { it.displayName }.toTypedArray() + + AlertDialog.Builder(context) + .setTitle(titleRes) + .setItems(fontNames) { _, which -> + val selectedFont = fonts[which].fileName + onFontSelected(selectedFont) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + /** + * 更新字体配置文件 + */ + fun updateFontConfiguration() { + try { + val fontPrefs = AppPrefs.getInstance().fonts + val config = JSONObject().apply { + val candFont = fontPrefs.candFont.getValue() + val preeditFont = fontPrefs.preeditFont.getValue() + val popupKeyFont = fontPrefs.popupKeyFont.getValue() + val keyMainFont = fontPrefs.keyMainFont.getValue() + val keyAltFont = fontPrefs.keyAltFont.getValue() + val defaultFont = fontPrefs.defaultFont.getValue() + + put("cand_font", candFont) + put("font", defaultFont) + put("preedit_font", preeditFont) + put("popup_key_font", popupKeyFont) + put("key_main_font", keyMainFont) + put("key_alt_font", keyAltFont) + } + + if (!fontsDir.exists()) { + fontsDir.mkdirs() + } + + fontConfigFile.writeText(config.toString(2)) + + // 清除字体缓存,触发重新加载 + AutoScaleTextView.clearFontCache() + } catch (e: Exception) { + // 记录错误但不崩溃 + e.printStackTrace() + } + } + + /** + * 字体信息 + */ + data class FontInfo( + val fileName: String, + val displayName: String + ) +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 6a4248401..d7e7f9589 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -359,6 +359,35 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { ) } + inner class Fonts : ManagedPreferenceCategory(R.string.fonts, sharedPreferences) { + val candFont: ManagedPreference.PString + val preeditFont: ManagedPreference.PString + val popupKeyFont: ManagedPreference.PString + val keyMainFont: ManagedPreference.PString + val keyAltFont: ManagedPreference.PString + val defaultFont: ManagedPreference.PString + + init { + candFont = ManagedPreference.PString(sharedPreferences, "cand_font", "").apply { register() } + preeditFont = ManagedPreference.PString(sharedPreferences, "preedit_font", "").apply { register() } + popupKeyFont = ManagedPreference.PString(sharedPreferences, "popup_key_font", "").apply { register() } + keyMainFont = ManagedPreference.PString(sharedPreferences, "key_main_font", "").apply { register() } + keyAltFont = ManagedPreference.PString(sharedPreferences, "key_alt_font", "").apply { register() } + defaultFont = ManagedPreference.PString(sharedPreferences, "font", "").apply { register() } + + val onChange = ManagedPreference.OnChangeListener { _, _ -> + org.fcitx.fcitx5.android.data.fonts.FontManager.updateFontConfiguration() + } + + candFont.registerOnChangeListener(onChange) + preeditFont.registerOnChangeListener(onChange) + popupKeyFont.registerOnChangeListener(onChange) + keyMainFont.registerOnChangeListener(onChange) + keyAltFont.registerOnChangeListener(onChange) + defaultFont.registerOnChangeListener(onChange) + } + } + private val providers = mutableListOf() fun registerProvider( @@ -378,6 +407,7 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { val candidates = Candidates().register() val clipboard = Clipboard().register() val symbols = Symbols().register() + val fonts = Fonts().register() val advanced = Advanced().register() @Keep diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt new file mode 100644 index 000000000..c700bc5af --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.data.prefs + +import android.content.SharedPreferences +import androidx.annotation.StringRes +import androidx.preference.PreferenceScreen +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.fonts.FontManager +import org.fcitx.fcitx5.android.utils.addPreference + +class FontPrefs(sharedPreferences: SharedPreferences) : ManagedPreferenceInternal(sharedPreferences) { + + // 字体配置项 + val candFont = string("cand_font", "") + val preeditFont = string("preedit_font", "") + val popupKeyFont = string("popup_key_font", "") + val keyMainFont = string("key_main_font", "") + val keyAltFont = string("key_alt_font", "") + val defaultFont = string("font", "") + + override fun createUi(screen: PreferenceScreen) { + val ctx = screen.context + + screen.addPreference( + R.string.font_candidate, + summary = getFontDisplayName(candFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_candidate, candFont) } + ) + + screen.addPreference( + R.string.font_preedit, + summary = getFontDisplayName(preeditFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_preedit, preeditFont) } + ) + + screen.addPreference( + R.string.font_popup_key, + summary = getFontDisplayName(popupKeyFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_popup_key, popupKeyFont) } + ) + + screen.addPreference( + R.string.font_key_main, + summary = getFontDisplayName(keyMainFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_key_main, keyMainFont) } + ) + + screen.addPreference( + R.string.font_key_alt, + summary = getFontDisplayName(keyAltFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_key_alt, keyAltFont) } + ) + + screen.addPreference( + R.string.font_default, + summary = getFontDisplayName(defaultFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_default, defaultFont) } + ) + } + + private fun getFontDisplayName(fontFileName: String): String { + return if (fontFileName.isEmpty()) { + "System Default" + } else { + fontFileName.substringBeforeLast('.') + } + } + + private fun showFontPicker( + context: android.content.Context, + @StringRes titleRes: Int, + preference: ManagedPreference.PString + ) { + FontManager.showFontPickerDialog(context, titleRes) { selectedFont -> + preference.setValue(selectedFont) + FontManager.updateFontConfiguration() + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index 03f39c334..3571f14cb 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -75,9 +75,14 @@ class AutoScaleTextView @JvmOverloads constructor( .let { json -> json.keys().asSequence().associateTo(mutableMapOf()) { key -> key to runCatching { - File(fontsDir, json.getString(key)) - .takeIf { it.exists() } - ?.let { Typeface.createFromFile(it) } + val fontFileName = json.getString(key) + if (fontFileName.isEmpty()) { + null // 使用系统默认字体 + } else { + File(fontsDir, fontFileName) + .takeIf { it.exists() } + ?.let { Typeface.createFromFile(it) } + } }.getOrNull() } } as MutableMap // 确保返回可变Map @@ -86,6 +91,15 @@ class AutoScaleTextView @JvmOverloads constructor( } return cachedFontTypefaceMap ?: mutableMapOf() } + + /** + * 清除字体缓存,强制重新加载字体配置 + */ + @Synchronized + fun clearFontCache() { + cachedFontTypefaceMap = null + lastModified = 0L + } } fun setFontTypeFace(key: String) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt index 20334a125..df99b5e97 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt @@ -85,6 +85,11 @@ class MainFragment : PaddingPreferenceFragment() { R.drawable.ic_baseline_emoji_symbols_24, SettingsRoute.Symbol ) + addDestinationPreference( + R.string.fonts, + R.drawable.ic_baseline_text_format_24, + SettingsRoute.Fonts + ) addDestinationPreference( R.string.plugins, R.drawable.ic_baseline_android_24, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt index 0ca9fac65..7481e08c2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt @@ -28,6 +28,7 @@ import org.fcitx.fcitx5.android.ui.main.settings.addon.AddonListFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.AdvancedSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.CandidatesSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.ClipboardSettingsFragment +import org.fcitx.fcitx5.android.ui.main.settings.behavior.FontsSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.KeyboardSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.SymbolSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.global.GlobalConfigFragment @@ -80,6 +81,9 @@ sealed class SettingsRoute : Parcelable { @Serializable data object Symbol : SettingsRoute() + @Serializable + data object Fonts : SettingsRoute() + @Serializable data object Plugin : SettingsRoute() @@ -219,6 +223,9 @@ sealed class SettingsRoute : Parcelable { fragment { label = ctx.getString(R.string.emoji_and_symbols) } + fragment { + label = ctx.getString(R.string.fonts) + } fragment { label = ctx.getString(R.string.plugins) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt new file mode 100644 index 000000000..c2354970e --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.ui.main.settings.behavior + +import android.os.Bundle +import androidx.preference.PreferenceScreen +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.fonts.FontManager +import org.fcitx.fcitx5.android.data.prefs.AppPrefs +import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceFragment +import org.fcitx.fcitx5.android.utils.addPreference + +class FontsSettingsFragment : ManagedPreferenceFragment(AppPrefs.getInstance().fonts) { + + override fun onPreferenceUiCreated(screen: PreferenceScreen) { + super.onPreferenceUiCreated(screen) + val fontPrefs = AppPrefs.getInstance().fonts + + screen.addPreference( + R.string.font_candidate, + summary = getFontDisplayName(fontPrefs.candFont.getValue()), + onClick = { showFontPicker(R.string.font_candidate, fontPrefs.candFont) } + ) + + screen.addPreference( + R.string.font_preedit, + summary = getFontDisplayName(fontPrefs.preeditFont.getValue()), + onClick = { showFontPicker(R.string.font_preedit, fontPrefs.preeditFont) } + ) + + screen.addPreference( + R.string.font_popup_key, + summary = getFontDisplayName(fontPrefs.popupKeyFont.getValue()), + onClick = { showFontPicker(R.string.font_popup_key, fontPrefs.popupKeyFont) } + ) + + screen.addPreference( + R.string.font_key_main, + summary = getFontDisplayName(fontPrefs.keyMainFont.getValue()), + onClick = { showFontPicker(R.string.font_key_main, fontPrefs.keyMainFont) } + ) + + screen.addPreference( + R.string.font_key_alt, + summary = getFontDisplayName(fontPrefs.keyAltFont.getValue()), + onClick = { showFontPicker(R.string.font_key_alt, fontPrefs.keyAltFont) } + ) + + screen.addPreference( + R.string.font_default, + summary = getFontDisplayName(fontPrefs.defaultFont.getValue()), + onClick = { showFontPicker(R.string.font_default, fontPrefs.defaultFont) } + ) + } + + private fun getFontDisplayName(fontFileName: String): String { + return if (fontFileName.isEmpty()) { + getString(R.string.system_default) + } else { + fontFileName.substringBeforeLast('.') + } + } + + private fun showFontPicker( + titleRes: Int, + preference: org.fcitx.fcitx5.android.data.prefs.ManagedPreference.PString + ) { + FontManager.showFontPickerDialog(requireContext(), titleRes) { selectedFont -> + preference.setValue(selectedFont) + // 刷新设置界面以显示新的摘要 + preferenceScreen.removeAll() + onCreatePreferences(null, null) + } + } +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 87b7ebd6e..10600ea6d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -311,4 +311,13 @@ 🏽 中等色 🏾 深色 🏿 非常深色 + + + 字体 + 候选字体 + 编码字体 + 弹出按键字体 + 按键主字体 + 按键次字体 + 默认字体 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ab841db5..2f76e9d5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -315,4 +315,13 @@ 🏾 Type-5 🏿 Type-6 Hide unsupported Emojis + + + Fonts + Candidate font + Preedit font + Popup key font + Key main font + Key alt font + Default font From 85433ea90c682752fe5846e313447a13bdd3ba49 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:36:35 +0000 Subject: [PATCH 17/20] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD=E4=BB=A5=E8=B7=9F?= =?UTF-8?q?=E8=B8=AA=E5=AD=97=E4=BD=93=E9=85=8D=E7=BD=AE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fcitx5/android/data/fonts/FontManager.kt | 32 +++++++++++++++++-- .../fcitx5/android/data/prefs/AppPrefs.kt | 4 ++- .../behavior/FontsSettingsFragment.kt | 2 ++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt index fcd82a0a9..3b336ae9d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt @@ -6,6 +6,7 @@ package org.fcitx.fcitx5.android.data.fonts import android.app.AlertDialog import android.content.Context +import android.util.Log import androidx.annotation.StringRes import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.prefs.AppPrefs @@ -16,6 +17,8 @@ import java.io.File object FontManager { + private const val TAG = "FontManager" + private val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") private val fontConfigFile = File(fontsDir, "fontset.json") @@ -72,6 +75,8 @@ object FontManager { */ fun updateFontConfiguration() { try { + Log.d(TAG, "开始更新字体配置") + val fontPrefs = AppPrefs.getInstance().fonts val config = JSONObject().apply { val candFont = fontPrefs.candFont.getValue() @@ -81,6 +86,14 @@ object FontManager { val keyAltFont = fontPrefs.keyAltFont.getValue() val defaultFont = fontPrefs.defaultFont.getValue() + Log.d(TAG, "读取到的字体配置:") + Log.d(TAG, " - candFont: '$candFont'") + Log.d(TAG, " - preeditFont: '$preeditFont'") + Log.d(TAG, " - popupKeyFont: '$popupKeyFont'") + Log.d(TAG, " - keyMainFont: '$keyMainFont'") + Log.d(TAG, " - keyAltFont: '$keyAltFont'") + Log.d(TAG, " - defaultFont: '$defaultFont'") + put("cand_font", candFont) put("font", defaultFont) put("preedit_font", preeditFont) @@ -89,17 +102,32 @@ object FontManager { put("key_alt_font", keyAltFont) } + Log.d(TAG, "目标目录: ${fontsDir.absolutePath}") + Log.d(TAG, "目标文件: ${fontConfigFile.absolutePath}") + if (!fontsDir.exists()) { - fontsDir.mkdirs() + Log.d(TAG, "创建字体目录") + val created = fontsDir.mkdirs() + Log.d(TAG, "目录创建结果: $created") } + Log.d(TAG, "目录是否存在: ${fontsDir.exists()}") + Log.d(TAG, "目录是否可写: ${fontsDir.canWrite()}") + fontConfigFile.writeText(config.toString(2)) + Log.d(TAG, "文件是否存在: ${fontConfigFile.exists()}") + Log.d(TAG, "文件大小: ${if (fontConfigFile.exists()) fontConfigFile.length() else "N/A"}") + + // 添加日志信息 + Log.i(TAG, "字体配置已更新到文件: ${fontConfigFile.absolutePath}") + Log.d(TAG, "配置内容: ${config.toString(2)}") + // 清除字体缓存,触发重新加载 AutoScaleTextView.clearFontCache() } catch (e: Exception) { // 记录错误但不崩溃 - e.printStackTrace() + Log.e(TAG, "更新字体配置时出错: ${e.message}", e) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index d7e7f9589..ab592c1a7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -6,6 +6,7 @@ package org.fcitx.fcitx5.android.data.prefs import android.content.SharedPreferences import android.os.Build +import android.util.Log import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.core.content.edit @@ -375,7 +376,8 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { keyAltFont = ManagedPreference.PString(sharedPreferences, "key_alt_font", "").apply { register() } defaultFont = ManagedPreference.PString(sharedPreferences, "font", "").apply { register() } - val onChange = ManagedPreference.OnChangeListener { _, _ -> + val onChange = ManagedPreference.OnChangeListener { key, value -> + Log.d("AppPrefs.Fonts", "字体配置更改 - $key: $value") org.fcitx.fcitx5.android.data.fonts.FontManager.updateFontConfiguration() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt index c2354970e..d4bd730fa 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt @@ -69,6 +69,8 @@ class FontsSettingsFragment : ManagedPreferenceFragment(AppPrefs.getInstance().f ) { FontManager.showFontPickerDialog(requireContext(), titleRes) { selectedFont -> preference.setValue(selectedFont) + // 手动触发字体配置更新 + FontManager.updateFontConfiguration() // 刷新设置界面以显示新的摘要 preferenceScreen.removeAll() onCreatePreferences(null, null) From fc687c6b61a15ba996569cf8902e77616464e504 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:46:08 +0000 Subject: [PATCH 18/20] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E6=9B=B4=E6=94=B9=E7=9B=91=E5=90=AC=E5=99=A8=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8A=A8=E6=80=81=E6=9B=B4=E6=96=B0=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fcitx5/android/data/fonts/FontManager.kt | 38 +++++++++++++++++++ .../android/input/FcitxInputMethodService.kt | 11 ++++++ 2 files changed, 49 insertions(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt index 3b336ae9d..e2f6d9886 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt @@ -21,6 +21,13 @@ object FontManager { private val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") private val fontConfigFile = File(fontsDir, "fontset.json") + + // 字体更改监听器 + interface OnFontChangeListener { + fun onFontConfigurationChanged() + } + + private val fontChangeListeners = mutableListOf() init { if (!fontsDir.exists()) { @@ -28,6 +35,34 @@ object FontManager { } } + /** + * 添加字体更改监听器 + */ + fun addOnFontChangeListener(listener: OnFontChangeListener) { + fontChangeListeners.add(listener) + } + + /** + * 移除字体更改监听器 + */ + fun removeOnFontChangeListener(listener: OnFontChangeListener) { + fontChangeListeners.remove(listener) + } + + /** + * 通知所有监听器字体配置已更改 + */ + private fun notifyFontConfigurationChanged() { + Log.d(TAG, "通知字体配置更改,监听器数量: ${fontChangeListeners.size}") + fontChangeListeners.forEach { listener -> + try { + listener.onFontConfigurationChanged() + } catch (e: Exception) { + Log.e(TAG, "字体更改监听器执行失败", e) + } + } + } + /** * 获取可用字体列表 */ @@ -125,6 +160,9 @@ object FontManager { // 清除字体缓存,触发重新加载 AutoScaleTextView.clearFontCache() + + // 通知所有监听器字体配置已更改 + notifyFontConfigurationChanged() } catch (e: Exception) { // 记录错误但不崩溃 Log.e(TAG, "更新字体配置时出错: ${e.message}", e) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index b417f5a64..8c24228ac 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -57,6 +57,7 @@ import org.fcitx.fcitx5.android.core.SubtypeManager import org.fcitx.fcitx5.android.daemon.FcitxConnection import org.fcitx.fcitx5.android.daemon.FcitxDaemon import org.fcitx.fcitx5.android.data.InputFeedbacks +import org.fcitx.fcitx5.android.data.fonts.FontManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceProvider @@ -170,6 +171,14 @@ class FcitxInputMethodService : LifecycleInputMethodService() { replaceInputViews(it) } + @Keep + private val fontChangeListener = object : FontManager.OnFontChangeListener { + override fun onFontConfigurationChanged() { + // 字体配置更改时重新创建输入视图以应用新字体 + replaceInputView(ThemeManager.activeTheme) + } + } + /** * Post a fcitx operation to [jobs] to be executed * @@ -201,6 +210,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } prefs.candidates.registerOnChangeListener(recreateCandidatesViewListener) ThemeManager.addOnChangedListener(onThemeChangeListener) + FontManager.addOnFontChangeListener(fontChangeListener) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { postFcitxJob { SubtypeManager.syncWith(enabledIme()) @@ -989,6 +999,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } prefs.candidates.unregisterOnChangeListener(recreateCandidatesViewListener) ThemeManager.removeOnChangedListener(onThemeChangeListener) + FontManager.removeOnFontChangeListener(fontChangeListener) super.onDestroy() // Fcitx might be used in super.onDestroy() FcitxDaemon.disconnect(javaClass.name) From 797c6d000d823aa57f8dc03acdc2b199b10fc1a6 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:51:36 +0000 Subject: [PATCH 19/20] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20CI=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BB=A5=E6=94=AF=E6=8C=81=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E8=A7=A6=E5=8F=91=E6=9E=84=E5=BB=BA=E5=B9=B6?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=94=AF=E4=B8=80=E7=9A=84=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E5=92=8C=E8=AF=A6=E7=BB=86=E7=9A=84=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 66 +++++++++- .github/workflows/release.yml | 231 ++++++++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8ec7138d..e02a1352b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: branches: - '*' tags: - - '![0-9]+.*' + - '*' # 允许所有标签触发构建 paths: - '**/**' - '!*.md' @@ -126,13 +126,69 @@ jobs: path: app/build/outputs/apk/release/*.apk # create nightly release - - name: Create Nightly release + - name: Generate unique release tag + id: generate_tag + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + # 如果是 tag 触发,使用 tag 名称 + 时间戳确保唯一性 + RELEASE_TAG="${{ github.ref_name }}-$(date +'%Y%m%d-%H%M%S')" + RELEASE_TITLE="Release ${{ github.ref_name }} ($(date +'%Y-%m-%d %H:%M:%S'))" + IS_PRERELEASE="false" + else + # 如果是分支触发,创建 nightly 构建 + RELEASE_TAG="nightly-${{ github.ref_name }}-$(date +'%Y%m%d-%H%M%S')" + RELEASE_TITLE="Nightly Build - ${{ github.ref_name }} ($(date +'%Y-%m-%d %H:%M:%S'))" + IS_PRERELEASE="true" + fi + echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + echo "release_title=$RELEASE_TITLE" >> $GITHUB_OUTPUT + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + + - name: Generate release notes + id: generate_notes + run: | + # 获取最近的 commits 作为 release notes + if [[ "${{ github.ref_type }}" == "tag" ]]; then + # 对于 tag,获取从上一个 tag 到当前 tag 的变更 + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [[ -n "$PREVIOUS_TAG" ]]; then + RELEASE_NOTES=$(git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD) + else + RELEASE_NOTES=$(git log --pretty=format:"- %s (%h)" --max-count=10) + fi + else + # 对于分支,获取最近 10 个 commits + RELEASE_NOTES=$(git log --pretty=format:"- %s (%h)" --max-count=10) + fi + + # 添加构建信息 + BUILD_INFO=" + + ## 构建信息 + - 构建时间: $(date +'%Y-%m-%d %H:%M:%S UTC') + - 分支/标签: ${{ github.ref_name }} + - 提交: ${{ github.sha }} + - 构建系统: ${{ matrix.os }} + - 架构: ${{ matrix.abi }} + " + + FULL_NOTES="## 更改日志 + $RELEASE_NOTES + $BUILD_INFO" + + # 保存到文件,处理多行内容 + echo "$FULL_NOTES" > release_notes.md + echo "Generated release notes:" + cat release_notes.md + + - name: Create Release uses: 'marvinpinto/action-automatic-releases@latest' with: repo_token: ${{ secrets.GITHUB_TOKEN }} - automatic_release_tag: ${{ github.ref_name }} - prerelease: true - title: "Nightly Build" + automatic_release_tag: ${{ steps.generate_tag.outputs.release_tag }} + prerelease: ${{ steps.generate_tag.outputs.is_prerelease }} + title: ${{ steps.generate_tag.outputs.release_title }} + body_path: release_notes.md files: | app/build/outputs/apk/release/*.apk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..7fb8d5030 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,231 @@ +name: Release + +on: + push: + tags: + - 'v[0-9]+.*' # 匹配版本标签,如 v1.0.0, v2.1.3-beta 等 + workflow_dispatch: + inputs: + tag: + description: 'Tag name for release' + required: true + default: 'v0.0.0' + prerelease: + description: 'Mark as pre-release' + type: boolean + default: false + +jobs: + release: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-22.04 + abi: + - arm64-v8a + - armeabi-v7a + - x86_64 + env: + BUILD_ABI: ${{ matrix.abi }} + steps: + - name: Fetch source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Setup Android environment + uses: android-actions/setup-android@v3 + + - name: Install Android NDK + run: | + sdkmanager --install "cmake;3.22.1" + + - name: Install system dependencies + run: | + sudo apt update + sudo apt install extra-cmake-modules gettext + + - name: Prepare personal build sources + run: | + ./prepare_personal_build.sh + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Build Release APK + run: | + ./gradlew :app:assembleRelease + ./gradlew :assembleReleasePlugins + + - name: Move plugin APKs + shell: bash + run: | + for i in $(ls plugin) + do + if [ -d "plugin/${i}" ] + then + mv "plugin/${i}/build/outputs/apk/release"/*.apk "app/build/outputs/apk/release/" + fi + done + + - name: Sign all App + uses: kevin-david/zipalign-sign-android-release@v1.1.1 + id: sign_apk + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + alias: ${{ secrets.KEY_ALIAS }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + zipAlign: true + env: + BUILD_TOOLS_VERSION: "31.0.0" + + - name: Delete unsigned APKs + shell: bash + run: | + rm app/build/outputs/apk/release/*unsigned.apk + pushd app/build/outputs/apk/release + for file in *unsigned-*; do + new_name="${file/unsigned-/}" + mv -- "$file" "$new_name" + done + popd + + - name: Determine release info + id: release_info + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + TAG_NAME="${{ github.event.inputs.tag }}" + IS_PRERELEASE="${{ github.event.inputs.prerelease }}" + else + TAG_NAME="${{ github.ref_name }}" + # 判断是否为预发布版本(包含 alpha, beta, rc 等) + if [[ "$TAG_NAME" =~ (alpha|beta|rc|pre) ]]; then + IS_PRERELEASE="true" + else + IS_PRERELEASE="false" + fi + fi + + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + echo "release_title=Release $TAG_NAME" >> $GITHUB_OUTPUT + + - name: Generate comprehensive release notes + id: release_notes + run: | + TAG_NAME="${{ steps.release_info.outputs.tag_name }}" + + # 获取上一个版本标签 + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + + # 生成变更日志 + if [[ -n "$PREVIOUS_TAG" ]]; then + echo "# 🚀 Release $TAG_NAME" > release_notes.md + echo "" >> release_notes.md + echo "## 📝 更改内容" >> release_notes.md + echo "" >> release_notes.md + + # 获取提交信息并分类 + git log --pretty=format:"%s (%h)" $PREVIOUS_TAG..HEAD | while read line; do + if [[ "$line" =~ ^(feat|feature) ]]; then + echo "### ✨ 新功能" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(fix|bugfix) ]]; then + echo "### 🐛 问题修复" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(docs|doc) ]]; then + echo "### 📚 文档更新" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(style|ui) ]]; then + echo "### 💄 界面改进" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(perf|performance) ]]; then + echo "### ⚡ 性能优化" >> release_notes.md + echo "- $line" >> release_notes.md + else + echo "### 🔧 其他更改" >> release_notes.md + echo "- $line" >> release_notes.md + fi + done + else + echo "# 🚀 Release $TAG_NAME" > release_notes.md + echo "" >> release_notes.md + echo "## 📝 初始版本" >> release_notes.md + echo "" >> release_notes.md + echo "这是项目的初始发布版本。" >> release_notes.md + fi + + # 添加构建信息 + echo "" >> release_notes.md + echo "## 🏗️ 构建信息" >> release_notes.md + echo "" >> release_notes.md + echo "- **构建时间**: $(date +'%Y-%m-%d %H:%M:%S UTC')" >> release_notes.md + echo "- **构建版本**: $TAG_NAME" >> release_notes.md + echo "- **提交 SHA**: \`${{ github.sha }}\`" >> release_notes.md + echo "- **构建架构**: ${{ matrix.abi }}" >> release_notes.md + + # 添加安装说明 + echo "" >> release_notes.md + echo "## 📱 安装说明" >> release_notes.md + echo "" >> release_notes.md + echo "1. 下载对应架构的 APK 文件" >> release_notes.md + echo "2. 在 Android 设备上启用「未知来源应用安装」" >> release_notes.md + echo "3. 安装主应用 APK" >> release_notes.md + echo "4. 根据需要安装插件 APK" >> release_notes.md + + echo "Generated release notes:" + cat release_notes.md + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts-${{ matrix.abi }} + path: app/build/outputs/apk/release/*.apk + + - name: Create Release + if: matrix.abi == 'arm64-v8a' # 只在一个架构上创建 release,避免重复 + uses: 'marvinpinto/action-automatic-releases@latest' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + automatic_release_tag: ${{ steps.release_info.outputs.tag_name }} + prerelease: ${{ steps.release_info.outputs.is_prerelease }} + title: ${{ steps.release_info.outputs.release_title }} + body_path: release_notes.md + files: | + app/build/outputs/apk/release/*.apk + + collect-artifacts: + if: always() + needs: release + runs-on: ubuntu-22.04 + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect all APKs + run: | + mkdir -p collected-apks + find artifacts -name "*.apk" -exec cp {} collected-apks/ \; + ls -la collected-apks/ + + - name: Update Release with all artifacts + if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch' + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name || github.event.inputs.tag }} + files: collected-apks/*.apk + fail_on_unmatched_files: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0bd1fea6cf1a7f5627a0623bd0152060f56c0d87 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 09:35:43 +0800 Subject: [PATCH 20/20] fix: Handle Enter key press from physical keyboard This change ensures that when the Enter key is pressed on a physical keyboard, it's processed by the `handleReturnKey()` function rather than being sent directly as a raw key event. This allows for custom handling of the Enter key. --- .../fcitx/fcitx5/android/input/FcitxInputMethodService.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 8c24228ac..bf9ad00d9 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -244,6 +244,11 @@ class FcitxInputMethodService : LifecycleInputMethodService() { // KeyEvent from physical keyboard (or input method engine forwardKey) // use cached event if available cachedKeyEvents.remove(it.timestamp)?.let { keyEvent -> + // If you also want cached Enter key presses to go through your custom logic: + if (keyEvent.keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) { + handleReturnKey() + return@event // Skip sending the raw keyEvent if handled + } currentInputConnection?.sendKeyEvent(keyEvent) return@event }