diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml deleted file mode 100644 index ea9554aea..000000000 --- a/.github/workflows/build-release-apk.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Build Release APKs - -on: - workflow_dispatch: - push: - tags: - - 'v*' - - 'beta-*' - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - - name: Decode Keystore - env: - RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }} - if: ${{ env.RELEASE_KEYSTORE != '' }} - run: | - echo "${{ secrets.RELEASE_KEYSTORE }}" | base64 -d > leantype-release.jks - echo "keyAlias=${{ secrets.RELEASE_KEY_ALIAS }}" > keystore.properties - echo "keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }}" >> keystore.properties - echo "storeFile=leantype-release.jks" >> keystore.properties - echo "storePassword=${{ secrets.RELEASE_STORE_PASSWORD }}" >> keystore.properties - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build Release APKs - run: | - if [[ "${{ github.ref_name }}" == beta-* ]]; then - ./gradlew assembleStandardfullRelease - else - ./gradlew assembleStandardRelease assembleStandardfullRelease assembleOfflineRelease - fi - - - name: Generate Release Notes - # ponytail: generate release notes from changelog during build - run: python3 docs/scripts/generate_release_notes.py - - - name: Upload APKs and Release Notes - uses: actions/upload-artifact@v4 - with: - name: LeanType-Release-APKs - path: | - app/build/outputs/apk/**/*.apk - docs/releasenote/release_notes_v*.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/beta-') - with: - prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }} - body_path: docs/releasenote/release_notes_temp.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..710958923 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,151 @@ +# Manual dispatch builds a signed candidate only. Version tags create a DRAFT, +# never a public release. Required secrets: KEYSTORE_BASE64, KEYSTORE_PASSWORD, +# KEY_ALIAS, KEY_PASSWORD (the existing LeanTypeDual signing identity). +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Validate release notes + run: | + python3 -B -m unittest discover -s docs/scripts -p 'test_*.py' -v + python3 docs/scripts/generate_release_notes.py + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - uses: gradle/actions/setup-gradle@v3 + + - name: Decode signing keystore + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: | + for name in KEYSTORE_BASE64 KEYSTORE_PASSWORD KEY_ALIAS KEY_PASSWORD; do + if [ -z "${!name}" ]; then + echo "::error::Required signing secret $name is not set." + exit 1 + fi + done + umask 077 + printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/release.jks" + python3 - <<'PY' + import os + from pathlib import Path + + # Java properties treat backslashes and leading spaces as syntax. + def escape(value): + encoded = value.encode("utf-16-be") + return "".join( + f"\\u{int.from_bytes(encoded[i:i + 2], 'big'):04x}" + for i in range(0, len(encoded), 2) + ) + + properties = { + "storeFile": os.environ["RUNNER_TEMP"] + "/release.jks", + "storePassword": os.environ["KEYSTORE_PASSWORD"], + "keyAlias": os.environ["KEY_ALIAS"], + "keyPassword": os.environ["KEY_PASSWORD"], + } + Path("keystore.properties").write_text( + "".join(f"{key}={escape(value)}\n" for key, value in properties.items()), + encoding="utf-8", + ) + PY + + - name: Build signed release APKs (three flavors, with R8) + run: | + chmod +x gradlew + ./gradlew :app:assembleStandardRelease :app:assembleStandardfullRelease :app:assembleOfflineRelease + + - name: Remove signing material + if: always() + run: rm -f keystore.properties "$RUNNER_TEMP/release.jks" + + - name: Verify and collect signed APKs + run: | + APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner -type f | sort -V | tail -1)" + test -x "$APKSIGNER" + shopt -s nullglob + all_apks=(app/build/outputs/apk/*/release/*.apk) + test "${#all_apks[@]}" -eq 3 + mkdir -p release-apks release-verification + for flavor in standard standardfull offline; do + apks=(app/build/outputs/apk/"$flavor"/release/*.apk) + test "${#apks[@]}" -eq 1 + apk="${apks[0]}" + [[ "$apk" == *"-${flavor}-release.apk" ]] + "$APKSIGNER" verify --verbose --print-certs "$apk" | tee "release-verification/$flavor.txt" + min_sdk=23 + if [ "$flavor" = offline ]; then min_sdk=21; fi + # Android 5/6 require v1 signing; modern-only signatures are insufficient. + "$APKSIGNER" verify --verbose --min-sdk-version "$min_sdk" --max-sdk-version 23 "$apk" \ + | tee -a "release-verification/$flavor.txt" + test -s "app/build/outputs/mapping/${flavor}Release/mapping.txt" + cp "$apk" release-apks/ + done + cp docs/releasenote/release_notes_temp.md release-apks/release_notes.md + printf '%s\n' "$GITHUB_SHA" > release-apks/commit.txt + (cd release-apks && sha256sum ./*.apk > SHA256SUMS.txt) + + - name: Upload signed candidate + uses: actions/upload-artifact@v4 + with: + name: release-apks + path: release-apks/ + if-no-files-found: error + + - name: Upload verification and R8 mappings + uses: actions/upload-artifact@v4 + with: + name: release-verification + path: | + release-verification/ + app/build/outputs/mapping/*Release/mapping.txt + if-no-files-found: error + + draft: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: release-apks + path: release-apks + + - name: Draft GitHub Release + uses: softprops/action-gh-release@v2 + with: + draft: true + prerelease: ${{ contains(github.ref_name, '-') }} + body_path: release-apks/release_notes.md + fail_on_unmatched_files: true + files: | + release-apks/*.apk + release-apks/SHA256SUMS.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..13a8e1e74 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# LeanTypeDual changelog + +## [Unreleased] + +## [2.0.0] - 2026-09-15 (release preparation) + +The date records preparation, not public release publication. + +### Added +- Optional swipe-up and swipe-down shortcut menus, aligned with the top and bottom character rows and editable through Secondary layouts. The upper gesture follows the number row when shown. (#156) + +### Fixed +- Cancel queued glide updates when a swipe shortcut menu takes over the touch gesture. (#156) + +### Reliability & testing +- Exercise swipe menus with glide detection both enabled and disabled, including completed glide paths, pending timers and recovery after cancellation; clarify popup input completion and coordinate spaces. (#156) + +### Upstream +- Based on LeanBitLab/LeanType commit `2a9d5fecb67c12b834ba2a1e80ffcdc8ab7d6720`; upstream release history remains in `docs/releasenote/`. + +### Changed +- Reset onto the upstream baseline rather than carrying forward the old fork's custom functionality. This is not feature parity with 0.3.0, and complete settings migration is not guaranteed. +- Brand the app, keyboard, spell checker, settings, and APKs as LeanTypeDual, using package `com.asafmah.leantypedual` and version `2.0.0` (`6000`) while retaining upstream icons and plugin compatibility. +- Check this fork's releases for app updates instead of offering upstream LeanType APKs. +- Prepare signed standard, standardfull, and offline APKs using the fork's release workflow. Manual runs stage candidates only; version tags create drafts, never public releases. No offlinelite APK is produced. diff --git a/README.md b/README.md index 4f49706db..4d0825a1e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# LeanType +# LeanTypeDual + +LeanTypeDual is [AsafMah/LeanType](https://github.com/AsafMah/LeanType), a fork of +[LeanBitLab/LeanType](https://github.com/LeanBitLab/LeanType), which is based on HeliBoard / OpenBoard / AOSP LatinIME. +It installs alongside upstream LeanType as `com.asafmah.leantypedual` (with `.offline` for the offline flavor). +LeanTypeDual releases are available from [this fork](https://github.com/AsafMah/LeanType/releases); +the upstream artwork, documentation, community links, and plugin ecosystem below retain their original provenance. @@ -16,7 +22,7 @@ [![Donate on Open Collective](https://img.shields.io/badge/Donate-Open_Collective-1f6feb?style=flat-square&logo=opencollective&logoColor=white)](https://opencollective.com/leanbitlab-org) **A private, smart, and deeply customizable open-source Android keyboard.** -*Forked from [HeliBoard](https://github.com/Helium314/HeliBoard) / OpenBoard / AOSP LatinIME.* +*Forked from [LeanType](https://github.com/LeanBitLab/LeanType), based on [HeliBoard](https://github.com/Helium314/HeliBoard) / OpenBoard / AOSP LatinIME.* [Screenshots](#-screenshots) • [Download APKs](#-download) • [Flavor Comparison](#-flavor-comparison) • [Features](#-features) • [Setup Guide](#-setup-guide) • [Ecosystem](#-ecosystem--plugins) • [Other Projects](https://github.com/LeanBitLab#-android-projects) @@ -26,7 +32,7 @@ ## 🚀 Overview -**LeanType** combines the trusted, lightweight, privacy-focused foundation of HeliBoard with modern productivity features: Multi-Provider Cloud, Self-Hosted & Offline AI proofreading, On-Device Whisper Voice Typing, Handwriting Recognition, In-Keyboard Offline Camera & Screenshot OCR, Real-time Inline Math Calculations, Zero-Latency Custom Sound Packs, Smart Toolbar Auto-Spanning, Built-in Self-Updater, and Rich Text Tools, while keeping you in complete control over your data. +**LeanTypeDual** combines the trusted, lightweight, privacy-focused foundation of HeliBoard with modern productivity features: Multi-Provider Cloud, Self-Hosted & Offline AI proofreading, On-Device Whisper Voice Typing, Handwriting Recognition, In-Keyboard Offline Camera & Screenshot OCR, Real-time Inline Math Calculations, Zero-Latency Custom Sound Packs, Smart Toolbar Auto-Spanning, Built-in Self-Updater, and Rich Text Tools, while keeping you in complete control over your data. --- @@ -47,7 +53,7 @@ ## 📦 Flavor Comparison -LeanType is available in **3 distinct flavors** designed to match your exact privacy preferences, hardware specifications, and feature requirements: +LeanTypeDual is available in **3 distinct flavors** designed to match your exact privacy preferences, hardware specifications, and feature requirements: | Feature / Capability | 🌟 Standard Full
`-standardfull-release.apk` | 🌿 Standard (FOSS)
`-standard-release.apk` | 🛡️ Offline
`-offline-release.apk` | | :--- | :---: | :---: | :---: | @@ -61,7 +67,7 @@ LeanType is available in **3 distinct flavors** designed to match your exact pri | **In-App Self-Updater** | ✅ **Yes** *(GitHub Releases)* | ❌ No *(F-Droid managed)* | ❌ No | | **Plugins & Models Setup** | In-app download or File import | In-app download or File import | Browser download + File import | | **Internet Permission** | 🌐 Optional *(Cloud AI/Updates)* | 🌐 Optional *(Cloud AI)* | 🚫 **None** *(OS-level blocked)* | -| **Package ID** | `com.leanbitlab.leantype` | `com.leanbitlab.leantype` | `com.leanbitlab.leantype.offline` | +| **Package ID** | `com.asafmah.leantypedual` | `com.asafmah.leantypedual` | `com.asafmah.leantypedual.offline` | | **Min Android Version** | Android 6.0+ *(SDK 23)* | Android 6.0+ *(SDK 23)* | Android 5.0+ *(SDK 21)* | | **Approximate APK Size** | **~10.8 MB** | **~10.8 MB** | **~9.8 MB** | @@ -150,7 +156,7 @@ LeanType is available in **3 distinct flavors** designed to match your exact pri ### 2. Voice Input Setup (On-Device Whisper AI) 1. Download and install the [LeanType Voice Plugin APK](https://github.com/LeanBitLab/LeanType-Voice-Plugin/releases/latest) on your Android device (installed as a background IPC service). 2. Grant **Microphone permission** to the LeanType Voice Plugin. -3. In LeanType, open **Settings → Voice typing** (or **Settings → Plugins → Voice**) and tap **Whisper Speech Models**. +3. In LeanTypeDual, open **Settings → Voice typing** (or **Settings → Plugins → Voice**) and tap **Whisper Speech Models**. 4. Download or import your preferred Whisper model (e.g. *Base* ~74 MB recommended). 5. Tap the microphone icon on the keyboard toolbar to start speech-to-text! @@ -168,9 +174,9 @@ LeanType is available in **3 distinct flavors** designed to match your exact pri ### 5. Offline AI Setup (Local GGUF Models) 1. Download `ai_plugin-arm64-v8a.apk` (or `ai_plugin-x86_64.apk`) from the [LeanType Offline AI Plugin Releases](https://github.com/LeanBitLab/LeanType-Offline-AI-Plugin/releases/latest). -2. In LeanType (`offline` build), navigate to **Settings → Plugins → Offline AI** and tap **Load Offline AI plugin** to load the `.apk`. +2. In LeanTypeDual (`offline` build), navigate to **Settings → Plugins → Offline AI** and tap **Load Offline AI plugin** to load the `.apk`. 3. Download a compatible GGUF model (e.g. `Qwen2.5-0.5B-Instruct-Q4_K_M.gguf` or `Llama-3.2-1B-Instruct-Q4_K_M.gguf`). -4. In LeanType, navigate to **Settings → Advanced → GGUF Model (.gguf)** and select the model file from storage. +4. In LeanTypeDual, navigate to **Settings → Advanced → GGUF Model (.gguf)** and select the model file from storage. ### 6. Dictionaries & Gesture Typing Setup 1. **Dictionaries**: With unbundled dictionaries in v4.1.6, open **Settings → Dictionaries** (or tap the dictionary icon on the toolbar when missing) to download or import your language dictionary (`.dict`). @@ -185,7 +191,7 @@ LeanType is available in **3 distinct flavors** designed to match your exact pri ## 🧩 Ecosystem & Plugins -Expand LeanType with official companion plugins: +Expand LeanTypeDual with upstream LeanType companion plugins: | Plugin | Repository | Description | | :--- | :--- | :--- | @@ -245,5 +251,5 @@ If LeanType improves your daily typing workflow, please consider sponsoring our ## ⚖️ License -LeanType is licensed under the **GNU General Public License v3.0 (GPL-3.0)**. +LeanTypeDual is licensed under the **GNU General Public License v3.0 (GPL-3.0)**. See the [LICENSE](LICENSE) file for details. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 92a4a4530..7bfdb9e98 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,12 +20,12 @@ android { compileSdk = 36 defaultConfig { - applicationId = "com.leanbitlab.leantype" + applicationId = "com.asafmah.leantypedual" minSdk = 21 targetSdk = 35 - // ponytail: release version 4.2.2 - versionCode = 4202 - versionName = "4.2.2" + // LeanTypeDual: 4000 + major * 1000 + minor * 100 + patch * 10. + versionCode = 6000 + versionName = "2.0.0" proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") @@ -113,7 +113,7 @@ android { if (number.isNotEmpty()) { outputs.all { val output = this as? com.android.build.gradle.api.ApkVariantOutput - output?.outputFileName = "$number-LeanType_${defaultConfig.versionName}-${flavor}-${buildType.name}.apk" + output?.outputFileName = "$number-LeanTypeDual_${defaultConfig.versionName}-${flavor}-${buildType.name}.apk" } } diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index fb020d9ee..5f3e85a1c 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -3,8 +3,8 @@ SPDX-License-Identifier: GPL-3.0-only --> - LeanType debug - LeanType debug Spell Checker - LeanType debug Settings + LeanTypeDual debug + LeanTypeDual debug Spell Checker + LeanTypeDual debug Settings diff --git a/app/src/debugNoMinify/res/values/strings.xml b/app/src/debugNoMinify/res/values/strings.xml index 50ecf244d..1db53ed8f 100644 --- a/app/src/debugNoMinify/res/values/strings.xml +++ b/app/src/debugNoMinify/res/values/strings.xml @@ -3,7 +3,7 @@ SPDX-License-Identifier: GPL-3.0-only --> - LeanType debug - LeanType debug Spell Checker - LeanType debug Settings + LeanTypeDual debug + LeanTypeDual debug Spell Checker + LeanTypeDual debug Settings diff --git a/app/src/main/assets/layouts/swipe_down/swipe_down.json b/app/src/main/assets/layouts/swipe_down/swipe_down.json new file mode 100644 index 000000000..4b16fa53c --- /dev/null +++ b/app/src/main/assets/layouts/swipe_down/swipe_down.json @@ -0,0 +1,11 @@ +[ + [ + { "label": "left" }, + { "label": "right" }, + { "label": "word_left" }, + { "label": "word_right" }, + { "label": "clipboard" }, + { "label": "numpad" }, + { "label": "emoji" } + ] +] diff --git a/app/src/main/assets/layouts/swipe_up/swipe_up.json b/app/src/main/assets/layouts/swipe_up/swipe_up.json new file mode 100644 index 000000000..b7c71eab9 --- /dev/null +++ b/app/src/main/assets/layouts/swipe_up/swipe_up.json @@ -0,0 +1,11 @@ +[ + [ + { "label": "undo" }, + { "label": "redo" }, + { "label": "cut" }, + { "label": "copy" }, + { "label": "paste" }, + { "label": "select_all" }, + { "label": "clipboard" } + ] +] diff --git a/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt b/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt index a35133395..b33a76ae3 100644 --- a/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt +++ b/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt @@ -87,13 +87,13 @@ class PopupKeysKeyboardAccessibilityDelegate( mKeyboardView.onUpEvent(x, y, pointerId, eventTime) // TODO: Should fix this reference. This is a hack to clear the state of // {@link PointerTracker}. - PointerTracker.dismissAllPopupKeysPanels() + PointerTracker.finishAllPopupKeysInput() return } // Close the popup keys keyboard. // TODO: Should fix this reference. This is a hack to clear the state of // {@link PointerTracker}. - PointerTracker.dismissAllPopupKeysPanels() + PointerTracker.finishAllPopupKeysInput() } companion object { diff --git a/app/src/main/java/helium314/keyboard/keyboard/Keyboard.kt b/app/src/main/java/helium314/keyboard/keyboard/Keyboard.kt index e6769a050..92e7d7f9b 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/Keyboard.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/Keyboard.kt @@ -57,6 +57,8 @@ open class Keyboard { val mAltCodeKeysWhileTyping: List val mIconsSet: KeyboardIconsSet + internal val swipeShortcutRows by lazy { SwipeShortcutMenu.findRows(this) } + private val mKeyCache = SparseArray() private val mProximityInfo: ProximityInfo diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.kt b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.kt index b960a21c1..852c65a0a 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.kt @@ -34,6 +34,7 @@ import helium314.keyboard.latin.settings.DebugSettings import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.utils.* +import helium314.keyboard.settings.FeedbackManager import java.util.Locale import java.util.WeakHashMap @@ -292,6 +293,48 @@ class MainKeyboardView @JvmOverloads constructor( return popupKeysKeyboardView } + override fun showSwipeShortcutMenu( + tracker: PointerTracker, direction: SwipeShortcutMenu.Direction + ): PopupKeysPanel? { + val sourceKeyboard = keyboard ?: return null + val sourceRow = sourceKeyboard.swipeShortcutRows[direction] ?: return null + val menu = try { + SwipeShortcutMenu.Builder(context, sourceKeyboard, sourceRow, direction).build() + } catch (e: IllegalArgumentException) { + reportInvalidSwipeLayout(direction, e) + return null + } catch (e: IllegalStateException) { + reportInvalidSwipeLayout(direction, e) + return null + } + val container = mPopupKeysKeyboardContainer + container.findViewById(R.id.description_view)?.visibility = View.GONE + val popup = container.findViewById(R.id.popup_keys_keyboard_view) + popup.setKeyboard(menu) + container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + container.layout(0, 0, container.measuredWidth, container.measuredHeight) + val downInView = CoordinateUtils.newInstance() + tracker.getDownCoordinates(downInView) + val menuTopInKeyboard = if (direction == SwipeShortcutMenu.Direction.UP) + sourceRow.top - menu.mOccupiedHeight - sourceKeyboard.mVerticalGap + else sourceRow.bottom + sourceKeyboard.mVerticalGap + popup.showSwipeShortcutPanel( + parentView = this, + controller = this, + keysLeftInParent = paddingLeft + sourceRow.left, + keysTopInParent = paddingTop + menuTopInKeyboard, + direction = direction, + startYInParent = CoordinateUtils.y(downInView), + listener = mKeyboardActionListener + ) + return popup + } + + private fun reportInvalidSwipeLayout(direction: SwipeShortcutMenu.Direction, error: RuntimeException) { + Log.e("SwipeShortcutMenu", "Invalid ${direction.layoutType} layout", error) + FeedbackManager.message(context, context.getString(R.string.layout_error, error.message)) + } + fun isInDraggingFinger(): Boolean = isShowingPopupKeysPanel() || PointerTracker.isAnyInDraggingFinger() override fun onShowPopupKeysPanel(panel: PopupKeysPanel) { @@ -304,7 +347,7 @@ class MainKeyboardView @JvmOverloads constructor( } fun isShowingPopupKeysPanel(): Boolean = mPopupKeysPanel?.isShowingInParent == true - override fun onCancelPopupKeysPanel() { PointerTracker.dismissAllPopupKeysPanels() } + override fun onCancelPopupKeysPanel() { PointerTracker.finishAllPopupKeysInput() } override fun onDismissPopupKeysPanel() { if (isShowingPopupKeysPanel()) { mPopupKeysPanel?.removeFromParent(); mPopupKeysPanel = null } } fun startDoubleTapShiftKeyTimer() { mTimerHandler.startDoubleTapShiftKeyTimer() } @@ -315,6 +358,7 @@ class MainKeyboardView @JvmOverloads constructor( override fun onTouchEvent(event: MotionEvent): Boolean { if (keyboard == null) return false if (mNonDistinctMultitouchHelper != null) { + if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) PointerTracker.cancelSwipeShortcutMenus() if (event.pointerCount > 1 && mTimerHandler.isInKeyRepeat()) mTimerHandler.cancelKeyRepeatTimers() mNonDistinctMultitouchHelper.processMotionEvent(event, mKeyDetector) return true @@ -323,6 +367,7 @@ class MainKeyboardView @JvmOverloads constructor( } fun processMotionEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) PointerTracker.cancelSwipeShortcutMenus() val index = event.actionIndex val id = event.getPointerId(index) val tracker = PointerTracker.getPointerTracker(id) @@ -337,7 +382,7 @@ class MainKeyboardView @JvmOverloads constructor( PointerTracker.setReleasedKeyGraphicsToAllKeys() mGestureFloatingTextDrawingPreview.dismissGestureFloatingPreviewText() mSlidingKeyInputDrawingPreview.dismissSlidingKeyInputPreview() - PointerTracker.dismissAllPopupKeysPanels() + PointerTracker.finishAllPopupKeysInput() dismissAllKeyPreviews() PointerTracker.cancelAllPointerTrackers() } diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt index 32b19c2e8..19b0e262f 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt @@ -114,6 +114,9 @@ class PointerTracker private constructor( private var mIsAllowedDraggingFinger = false // true if a keyswipe gesture is enabled and warranted. private var mKeySwipeAllowed = false + // The candidate is cleared when another gesture wins; an active menu consumes the pointer. + private var mSwipeShortcutCandidateKey: Key? = null + private var mSwipeShortcutActive = false private var mInTouchpadMode = false private var mTouchpadLastX = 0 @@ -230,6 +233,7 @@ class PointerTracker private constructor( if (keyDetector === mKeyDetector && keyboard === mKeyboard) { return } + cancelSwipeShortcut() val oldKeyboard = mKeyboard if (oldKeyboard != null) { // changing keyboards may change height @@ -361,7 +365,7 @@ class PointerTracker private constructor( Log.d(TAG, String.format(Locale.US, "[%d] onStartBatchInput", mPointerId)) } sListener.onStartBatchInput() - dismissAllPopupKeysPanels() + finishAllPopupKeysInput() getTimerProxy().cancelLongPressTimersOf(this) } @@ -471,6 +475,7 @@ class PointerTracker private constructor( } val key = getKeyOn(x, y) + cancelSwipeShortcutMenus() mBogusMoveEventDetector.onActualDownEvent(x, y) if (key != null && key.isModifier()) { if (sInGesture) { @@ -502,14 +507,25 @@ class PointerTracker private constructor( fun isShowingPopupKeysPanel(): Boolean = mPopupKeysPanel != null - private fun dismissPopupKeysPanel() { + private fun finishPopupKeysInput() { + if (mSwipeShortcutActive) { + mSwipeShortcutActive = false + // Dismissing or committing a menu must never fall through to typing its source key. + mIsTrackingForActionDisabled = true + } if (isShowingPopupKeysPanel()) { mPopupKeysPanel?.dismissPopupKeysPanel() mPopupKeysPanel = null } } + private fun cancelSwipeShortcut() { + mSwipeShortcutCandidateKey = null + if (mSwipeShortcutActive) finishPopupKeysInput() + } + private fun onDownEventInternal(x: Int, y: Int, eventTime: Long) { + mSwipeShortcutCandidateKey = null var key = onDownKey(x, y, eventTime) val isEmojiClipBottomRow = mKeyboard?.mId?.isEmojiClipBottomRow == true mIsAllowedDraggingFinger = (sParams?.mKeySelectionByDraggingFinger == true) || @@ -538,6 +554,11 @@ class PointerTracker private constructor( mStartX = x mStartY = y mStartTime = System.currentTimeMillis() + val settings = Settings.getValues() + if (getActivePointerTrackerCount() == 1 && !mKeySwipeAllowed && + (settings.mSwipeUpMenuEnabled || settings.mSwipeDownMenuEnabled)) { + mSwipeShortcutCandidateKey = key + } } } @@ -608,15 +629,18 @@ class PointerTracker private constructor( return } - if (sGestureEnabler.shouldHandleGesture() && me != null) { - // Add historical points to gesture path. + if (!isShowingPopupKeysPanel() && me != null && + (mSwipeShortcutCandidateKey != null || sGestureEnabler.shouldHandleGesture())) { + // Let the earliest decisive sample choose between shortcuts and glide. val pointerIndex = me.findPointerIndex(mPointerId) val historicalSize = me.historySize for (h in 0 until historicalSize) { val historicalX = me.getHistoricalX(pointerIndex, h).toInt() val historicalY = me.getHistoricalY(pointerIndex, h).toInt() val historicalTime = me.getHistoricalEventTime(h) - onGestureMoveEvent(historicalX, historicalY, historicalTime, false, null) + if (tryStartSwipeShortcut(historicalX, historicalY, historicalTime)) break + if (sGestureEnabler.shouldHandleGesture()) + onGestureMoveEvent(historicalX, historicalY, historicalTime, false, null) } } @@ -631,9 +655,41 @@ class PointerTracker private constructor( } return } + if (tryStartSwipeShortcut(x, y, eventTime)) return onMoveEventInternal(x, y, eventTime) } + private fun tryStartSwipeShortcut(x: Int, y: Int, eventTime: Long): Boolean { + val source = mSwipeShortcutCandidateKey ?: return false + if (sInGesture || sInKeySwipe || isShowingPopupKeysPanel() || + getActivePointerTrackerCount() != 1 || mIsTrackingForActionDisabled) { + mSwipeShortcutCandidateKey = null + return false + } + val dx = x - CoordinateUtils.x(mDownCoordinates) + val dy = y - CoordinateUtils.y(mDownCoordinates) + val threshold = maxOf(2 * sPointerStep, source.height / 4) + if (abs(dx) < threshold && abs(dy) < threshold) return false + mSwipeShortcutCandidateKey = null + if (abs(dy) <= abs(dx)) return false + val direction = if (dy < 0) SwipeShortcutMenu.Direction.UP else SwipeShortcutMenu.Direction.DOWN + val settings = Settings.getValues() + if (direction == SwipeShortcutMenu.Direction.UP && !settings.mSwipeUpMenuEnabled || + direction == SwipeShortcutMenu.Direction.DOWN && !settings.mSwipeDownMenuEnabled) return false + val row = mKeyboard?.swipeShortcutRows?.get(direction) ?: return false + if (source !in row.keys) return false + val panel = sDrawingProxy?.showSwipeShortcutMenu(this, direction) ?: return false + getTimerProxy().cancelKeyTimersOf(this) + getTimerProxy().cancelUpdateBatchInputTimer(this) + mIsDetectingGesture = false + setReleasedKeyGraphics(mCurrentKey, true) + mCurrentKey = null + mSwipeShortcutActive = true + mPopupKeysPanel = panel + panel.onDownEvent(panel.translateX(x), panel.translateY(y), mPointerId, eventTime) + return true + } + private fun processDraggingFingerInToNewKey(newKey: Key, x: Int, y: Int, eventTime: Long) { var key: Key? = newKey if (callListenerOnPressAndCheckKeyboardLayoutChange(newKey, 0)) { @@ -858,6 +914,7 @@ class PointerTracker private constructor( printTouchEvent("onUpEvent :", x, y, eventTime) } + tryStartSwipeShortcut(x, y, eventTime) getTimerProxy().cancelUpdateBatchInputTimer(this) if (!sInGesture) { if (mCurrentKey?.isModifier() == true) { @@ -879,6 +936,7 @@ class PointerTracker private constructor( } private fun onUpEventInternal(x: Int, y: Int, eventTime: Long) { + mSwipeShortcutCandidateKey = null getTimerProxy().cancelKeyTimersOf(this) val isInDraggingFinger = mIsInDraggingFinger val isInSlidingKeyInput = mIsInSlidingKeyInput @@ -903,7 +961,7 @@ class PointerTracker private constructor( panel.onUpEvent(translatedX, translatedY, mPointerId, eventTime) } } - dismissPopupKeysPanel() + finishPopupKeysInput() if (isInSlidingKeyInput) { callListenerOnFinishSlidingInput() } @@ -955,7 +1013,8 @@ class PointerTracker private constructor( } override fun cancelTrackingForAction() { - if (isShowingPopupKeysPanel()) { + mSwipeShortcutCandidateKey = null + if (isShowingPopupKeysPanel() && !mSwipeShortcutActive) { return } mIsTrackingForActionDisabled = true @@ -964,6 +1023,7 @@ class PointerTracker private constructor( val isInOperation: Boolean get() = !mIsTrackingForActionDisabled fun onLongPressed() { + mSwipeShortcutCandidateKey = null getTimerProxy().cancelLongPressTimersOf(this) if (isShowingPopupKeysPanel()) { return @@ -1028,7 +1088,7 @@ class PointerTracker private constructor( getTimerProxy().cancelKeyTimersOf(this) setReleasedKeyGraphics(mCurrentKey, true) resetKeySelectionByDraggingFinger() - dismissPopupKeysPanel() + finishPopupKeysInput() } private fun isMajorEnoughMoveToBeOnNewKey( @@ -1118,6 +1178,7 @@ class PointerTracker private constructor( } fun onKeyRepeat(code: Int, repeatCount: Int) { + mSwipeShortcutCandidateKey = null val key = key if (key == null || key.code != code) { mCurrentRepeatingKeyCode = Constants.NOT_A_CODE @@ -1270,6 +1331,12 @@ class PointerTracker private constructor( fun isAnyInDraggingFinger(): Boolean = sPointerTrackerQueue.isAnyInDraggingFinger() + fun cancelSwipeShortcutMenus() { + for (tracker in sTrackers) { + tracker.cancelSwipeShortcut() + } + } + fun cancelAllPointerTrackers() { sPointerTrackerQueue.cancelAllPointerTrackers() } @@ -1296,11 +1363,11 @@ class PointerTracker private constructor( } } - fun dismissAllPopupKeysPanels() { + fun finishAllPopupKeysInput() { val trackersSize = sTrackers.size for (i in 0 until trackersSize) { val tracker = sTrackers[i] - tracker.dismissPopupKeysPanel() + tracker.finishPopupKeysInput() } } diff --git a/app/src/main/java/helium314/keyboard/keyboard/PopupKeysKeyboardView.kt b/app/src/main/java/helium314/keyboard/keyboard/PopupKeysKeyboardView.kt index dd80ebcff..21038d3a5 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PopupKeysKeyboardView.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/PopupKeysKeyboardView.kt @@ -17,6 +17,7 @@ import android.view.MotionEvent import android.view.View import helium314.keyboard.accessibility.AccessibilityUtils import helium314.keyboard.accessibility.PopupKeysKeyboardAccessibilityDelegate +import helium314.keyboard.event.HapticEvent import helium314.keyboard.keyboard.emoji.EmojiViewCallback import helium314.keyboard.keyboard.internal.KeyDrawParams import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode @@ -26,6 +27,7 @@ import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.CoordinateUtils import kotlin.math.max import kotlin.math.min +import kotlin.math.abs /** * A view that renders a virtual [PopupKeysKeyboard]. It handles rendering of keys and @@ -44,9 +46,11 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( private var mController: PopupKeysPanel.Controller = PopupKeysPanel.EMPTY_CONTROLLER protected var mListener: KeyboardActionListener? = null protected var mEmojiViewCallback: EmojiViewCallback? = null - private var mOriginX = 0 - private var mOriginY = 0 + private var mOriginXInParent = 0 + private var mOriginYInParent = 0 private var mCurrentKey: Key? = null + private var mSwipeShortcutDirection: SwipeShortcutMenu.Direction? = null + private var mSwipeStartYInParent = 0 private var mActivePointerId = 0 @@ -97,6 +101,9 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( } override fun setKeyboard(keyboard: Keyboard) { + mCurrentKey?.onReleased() + mCurrentKey = null + mSwipeShortcutDirection = null super.setKeyboard(keyboard) mKeyDetector.setKeyboard( keyboard, @@ -132,6 +139,34 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( showPopupKeysPanelInternal(parentView, controller, pointX, pointY) } + fun showSwipeShortcutPanel( + parentView: View, + controller: PopupKeysPanel.Controller, + keysLeftInParent: Int, + keysTopInParent: Int, + direction: SwipeShortcutMenu.Direction, + startYInParent: Int, + listener: KeyboardActionListener + ) { + mListener = listener + mEmojiViewCallback = null + mController = controller + mSwipeShortcutDirection = direction + mSwipeStartYInParent = startYInParent + mOriginXInParent = keysLeftInParent - paddingLeft + mOriginYInParent = keysTopInParent - paddingTop + val container = getContainerView() + parentView.getLocationInWindow(mCoordinates) + val parentXInWindow = CoordinateUtils.x(mCoordinates) + val parentYInWindow = CoordinateUtils.y(mCoordinates) + // The preview container is window-relative; its child view includes background padding. + container.x = (parentXInWindow + mOriginXInParent - left).toFloat() + container.y = (parentYInWindow + mOriginYInParent - top).toFloat() + translationX = 0f + controller.setLayoutGravity(Gravity.CENTER_HORIZONTAL) + onPanelShown() + } + override fun showPopupKeysPanel( parentView: View, controller: PopupKeysPanel.Controller, @@ -190,9 +225,13 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( translationX = (panelFinalX - containerFinalX).toFloat() controller.setLayoutGravity(layoutGravity) - mOriginX = panelFinalX - mOriginY = y + container.paddingTop + this.y.toInt() - controller.onShowPopupKeysPanel(this) + mOriginXInParent = panelFinalX + mOriginYInParent = y + container.paddingTop + this.y.toInt() + onPanelShown() + } + + private fun onPanelShown() { + mController.onShowPopupKeysPanel(this) val accessibilityDelegate = mAccessibilityDelegate if (accessibilityDelegate != null && AccessibilityUtils.instance.isAccessibilityEnabled) { accessibilityDelegate.onShowPopupKeysKeyboard() @@ -245,6 +284,8 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( val listener = mListener if (listener != null) { val code = key.code + val isSwipeShortcut = mSwipeShortcutDirection != null + if (isSwipeShortcut) listener.onPressKey(code, 0, true, HapticEvent.NO_HAPTICS) if (code == KeyCode.MULTIPLE_CODE_POINTS) { listener.onTextInput(key.outputText) } else if (code != KeyCode.NOT_SPECIFIED) { @@ -259,6 +300,7 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( ) } } + if (isSwipeShortcut) listener.onReleaseKey(code, false) } else { mEmojiViewCallback?.onReleaseKey(key) } @@ -266,7 +308,8 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( private fun detectKey(x: Int, y: Int): Key? { val oldKey = mCurrentKey - val newKey = mKeyDetector.detectHitKey(x, y) + val newKey = if (mSwipeShortcutDirection == null) mKeyDetector.detectHitKey(x, y) + else detectSwipeShortcut(x, y) if (newKey === oldKey) { return newKey } @@ -282,6 +325,18 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( return newKey } + private fun detectSwipeShortcut(x: Int, y: Int): Key? { + val keyboard = keyboard ?: return null + val touchYInParent = y + mOriginYInParent + val outward = if (mSwipeShortcutDirection == SwipeShortcutMenu.Direction.UP) + touchYInParent < mSwipeStartYInParent else touchYInParent > mSwipeStartYInParent + val touchXInKeyboard = mKeyDetector.getTouchX(x) + if (!outward || touchXInKeyboard < 0 || touchXInKeyboard >= keyboard.mOccupiedWidth) return null + // Project onto the menu row so a short outward swipe already selects its aligned item. + return keyboard.sortedKeys.minByOrNull { abs(touchXInKeyboard - (it.x + it.width / 2)) } + ?.takeIf { it.isEnabled && !it.isSpacer } + } + private fun updateReleaseKeyGraphics(key: Key) { key.onReleased() invalidateKey(key) @@ -304,11 +359,11 @@ open class PopupKeysKeyboardView @JvmOverloads constructor( } override fun translateX(x: Int): Int { - return x - mOriginX + return x - mOriginXInParent } override fun translateY(y: Int): Int { - return y - mOriginY + return y - mOriginYInParent } @SuppressLint("ClickableViewAccessibility") diff --git a/app/src/main/java/helium314/keyboard/keyboard/SwipeShortcutMenu.kt b/app/src/main/java/helium314/keyboard/keyboard/SwipeShortcutMenu.kt new file mode 100644 index 000000000..05ba1be71 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/keyboard/SwipeShortcutMenu.kt @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard.keyboard + +import android.content.Context +import helium314.keyboard.keyboard.internal.KeyboardBuilder +import helium314.keyboard.keyboard.internal.keyboard_parser.LayoutParser +import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams +import helium314.keyboard.latin.common.Constants +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.LayoutType +import kotlin.math.floor +import kotlin.math.roundToInt + +/** Row geometry belongs to the keyboard, not to the key which opens the menu. */ +object SwipeShortcutMenu { + enum class Direction(val layoutType: LayoutType) { + UP(LayoutType.SWIPE_UP), DOWN(LayoutType.SWIPE_DOWN) + } + + class Row(val keys: List) { + val left = keys.first().x + val right = keys.last().x + keys.last().width + val top = keys.minOf { it.y } + val bottom = keys.maxOf { it.y + it.height } + + fun centers(count: Int): List { + require(count > 0 && count <= right - left) { "Too many swipe shortcuts for this row" } + return List(count) { index -> + val position = if (count == 1) keys.lastIndex / 2f + else index.toFloat() * keys.lastIndex / (count - 1) + val before = floor(position).toInt() + val after = (before + 1).coerceAtMost(keys.lastIndex) + val start = keys[before].x + keys[before].width / 2 + val end = keys[after].x + keys[after].width / 2 + (start + (end - start) * (position - before)).roundToInt() + } + } + } + + fun findRows(keyboard: Keyboard): Map { + // Custom menus may contain clipboard history or private text; mirror locked toolbar behavior. + if (!keyboard.mId.isAlphabetKeyboard || keyboard.mId.mDeviceLocked) return emptyMap() + // The space/punctuation row is functional; "." must keep its own long-press menu. + val spaceY = keyboard.getKey(Constants.CODE_SPACE)?.y ?: Int.MAX_VALUE + val characters = keyboard.sortedKeys.filter { + it.y < spaceY && it.isEnabled && !it.isSpacer && !it.isModifier() + && it.backgroundType == Key.BACKGROUND_TYPE_NORMAL + && (it.code > Constants.CODE_SPACE || !it.outputText.isNullOrEmpty()) + } + if (characters.isEmpty()) return emptyMap() + val top = characters.minOf { it.y } + val bottom = characters.maxOf { it.y } + return mapOf( + Direction.UP to Row(characters.filter { it.y == top }.sortedBy { it.x }), + Direction.DOWN to Row(characters.filter { it.y == bottom }.sortedBy { it.x }) + ) + } + + class Builder( + context: Context, + parent: Keyboard, + private val row: Row, + private val direction: Direction + ) : KeyboardBuilder( + context, PopupKeysKeyboard.PopupKeysKeyboardParams() + ) { + init { + mParams.mId = parent.mId + readAttributes(parent.mPopupKeysTemplate) + mParams.mLeftPadding = 0 + mParams.mRightPadding = 0 + mParams.mTopPadding = 0 + mParams.mBottomPadding = 0 + mParams.mHorizontalGap = 0 + mParams.mVerticalGap = 0 + mParams.mRelativeHorizontalGap = 0f + mParams.mRelativeVerticalGap = 0f + mParams.mOccupiedWidth = row.right - row.left + mParams.mBaseWidth = mParams.mOccupiedWidth + mParams.mOccupiedHeight = row.bottom - row.top + mParams.mBaseHeight = mParams.mOccupiedHeight + mParams.mDefaultRowHeight = 1f + addLocaleKeyTextsToParams(context, mParams, Settings.getValues().mShowMorePopupKeys) + } + + override fun build(): PopupKeysKeyboard { + val data = LayoutParser.parseLayout(direction.layoutType, mParams, mContext).flatten() + require(data.isNotEmpty()) { "Swipe shortcut layout is empty" } + val centers = row.centers(data.size) + val spacing = centers.zipWithNext { a, b -> b - a }.minOrNull() + ?: mParams.mBaseWidth + require(spacing > 0) { "Too many swipe shortcuts for this row" } + val width = minOf(row.keys.minOf { it.width }, spacing) + data.forEachIndexed { index, keyData -> + val keyParams = keyData.toKeyParams(mParams, Key.LABEL_FLAGS_AUTO_X_SCALE) + keyParams.xPos = (centers[index] - row.left - width / 2).toFloat() + keyParams.yPos = 0f + keyParams.mAbsoluteWidth = width.toFloat() + keyParams.mAbsoluteHeight = mParams.mBaseHeight.toFloat() + mParams.onAddKey(keyParams.createKey()) + } + return PopupKeysKeyboard(mParams) + } + } +} diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.kt index ccd5c00fb..abe91497f 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.kt @@ -9,11 +9,13 @@ package helium314.keyboard.keyboard.internal import helium314.keyboard.keyboard.Key import helium314.keyboard.keyboard.PopupKeysPanel import helium314.keyboard.keyboard.PointerTracker +import helium314.keyboard.keyboard.SwipeShortcutMenu interface DrawingProxy { fun onKeyPressed(key: Key, withPreview: Boolean) fun onKeyReleased(key: Key, withAnimation: Boolean) fun showPopupKeysKeyboard(key: Key, tracker: PointerTracker): PopupKeysPanel? + fun showSwipeShortcutMenu(tracker: PointerTracker, direction: SwipeShortcutMenu.Direction): PopupKeysPanel? fun startWhileTypingAnimation(fadeInOrOut: Int) fun showSlidingKeyInputPreview(tracker: PointerTracker?) fun showGestureTrail(tracker: PointerTracker, showsFloatingPreviewText: Boolean) diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt index ac164adaf..719c937c8 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt @@ -15,8 +15,8 @@ object Links { const val LICENSE = "$GITHUB/blob/main/LICENSE" const val SPONSOR = "https://github.com/sponsors/LeanBitLab" const val OPEN_COLLECTIVE = "https://opencollective.com/leanbitlab-org" - const val GITHUB_RELEASES_API = "https://api.github.com/repos/LeanBitLab/HeliboardL/releases/latest" - const val GITHUB_RELEASES_PAGE = "https://github.com/LeanBitLab/HeliboardL/releases" + const val GITHUB_RELEASES_API = "https://api.github.com/repos/AsafMah/LeanType/releases/latest" + const val GITHUB_RELEASES_PAGE = "https://github.com/AsafMah/LeanType/releases" const val FEATURES_URL = "$GITHUB/blob/main/docs/FEATURES.md" // Original HeliBoard wiki and community links const val ORIGINAL_GITHUB = "https://github.com/Helium314/HeliBoard" diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt index cb65ad424..d70cfe696 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -45,6 +45,8 @@ object Defaults { LayoutType.CUSTOM3 -> "symbols" LayoutType.CUSTOM4 -> "symbols" LayoutType.CUSTOM5 -> "symbols" + LayoutType.SWIPE_UP -> "swipe_up" + LayoutType.SWIPE_DOWN -> "swipe_down" } const val PREF_SPLIT_TOOLBAR = false @@ -124,6 +126,8 @@ object Defaults { const val PREF_SPACE_HORIZONTAL_SWIPE = "move_cursor" const val PREF_SPACE_VERTICAL_SWIPE = "touchpad_mode" const val PREF_DELETE_SWIPE = true + const val PREF_SWIPE_UP_MENU = false + const val PREF_SWIPE_DOWN_MENU = false const val PREF_AUTOSPACE_AFTER_PUNCTUATION = false const val PREF_AUTOSPACE_AFTER_EMOJI = false const val PREF_AUTOSPACE_AFTER_SUGGESTION = true diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt index 0f1c01fa5..4f91993f2 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt @@ -348,6 +348,8 @@ class Settings private constructor() : SharedPreferences.OnSharedPreferenceChang const val PREF_SPACE_HORIZONTAL_SWIPE = "horizontal_space_swipe" const val PREF_SPACE_VERTICAL_SWIPE = "vertical_space_swipe" const val PREF_DELETE_SWIPE = "delete_swipe" + const val PREF_SWIPE_UP_MENU = "swipe_up_menu" + const val PREF_SWIPE_DOWN_MENU = "swipe_down_menu" const val PREF_AUTOSPACE_AFTER_PUNCTUATION = "autospace_after_punctuation" const val PREF_AUTOSPACE_AFTER_EMOJI = "autospace_after_emoji" const val PREF_AUTOSPACE_AFTER_SUGGESTION = "autospace_after_suggestion" diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt index 995ac3798..44f35a646 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt @@ -91,6 +91,8 @@ open class SettingsValues( val mTouchpadFullscreen: Boolean val mForceAutoCaps: Boolean val mDeleteSwipeEnabled: Boolean + val mSwipeUpMenuEnabled: Boolean + val mSwipeDownMenuEnabled: Boolean val mAutospaceAfterPunctuation: Boolean val mAutospaceAfterEmoji: Boolean val mAutospaceAfterSuggestion: Boolean @@ -356,6 +358,8 @@ open class SettingsValues( mTouchpadFullscreen = prefs.getBoolean(Settings.PREF_TOUCHPAD_FULLSCREEN, Defaults.PREF_TOUCHPAD_FULLSCREEN) mForceAutoCaps = prefs.getBoolean(Settings.PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS) mDeleteSwipeEnabled = prefs.getBoolean(Settings.PREF_DELETE_SWIPE, Defaults.PREF_DELETE_SWIPE) + mSwipeUpMenuEnabled = prefs.getBoolean(Settings.PREF_SWIPE_UP_MENU, Defaults.PREF_SWIPE_UP_MENU) + mSwipeDownMenuEnabled = prefs.getBoolean(Settings.PREF_SWIPE_DOWN_MENU, Defaults.PREF_SWIPE_DOWN_MENU) mAutospaceAfterPunctuation = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION) mAutospaceAfterEmoji = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_EMOJI, Defaults.PREF_AUTOSPACE_AFTER_EMOJI) mAutospaceAfterSuggestion = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, Defaults.PREF_AUTOSPACE_AFTER_SUGGESTION) diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt index b14a91556..c0bd223d0 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt @@ -9,7 +9,7 @@ import java.util.EnumMap enum class LayoutType { MAIN, SYMBOLS, MORE_SYMBOLS, FUNCTIONAL, NUMBER, NUMBER_ROW, NUMPAD, NUMPAD_LANDSCAPE, PHONE, PHONE_SYMBOLS, EMOJI_BOTTOM, CLIPBOARD_BOTTOM, HANDWRITING_BOTTOM, EDITING, - CUSTOM1, CUSTOM2, CUSTOM3, CUSTOM4, CUSTOM5; + CUSTOM1, CUSTOM2, CUSTOM3, CUSTOM4, CUSTOM5, SWIPE_UP, SWIPE_DOWN; companion object { fun EnumMap.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY) @@ -45,6 +45,8 @@ enum class LayoutType { CUSTOM3 -> R.string.layout_custom3 CUSTOM4 -> R.string.layout_custom4 CUSTOM5 -> R.string.layout_custom5 + SWIPE_UP -> R.string.layout_swipe_up + SWIPE_DOWN -> R.string.layout_swipe_down } fun getMainLayoutFromExtraValue(extraValue: String): String? { diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt index c4d916d92..875c594e7 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt @@ -142,7 +142,7 @@ fun createAboutSettings(context: Context) = listOf( } ) { Text( - text = "LeanType is licensed under GNU GPL v3.0.\n\nThird-Party Speech Components:\n• whisper.cpp — MIT License (github.com/ggerganov/whisper.cpp)\n• Vosk Speech Recognition — Apache 2.0 (alphacephei.com/vosk)", + text = "LeanTypeDual is licensed under GNU GPL v3.0.\n\nThird-Party Speech Components:\n• whisper.cpp — MIT License (github.com/ggerganov/whisper.cpp)\n• Vosk Speech Recognition — Apache 2.0 (alphacephei.com/vosk)", style = MaterialTheme.typography.bodyMedium ) } diff --git a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt index 2480ff2f7..e4bd6d1da 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt @@ -169,7 +169,7 @@ fun ColorsScreen( }, stringResource(R.string.copy_to_clipboard) to { val cm = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip(ClipData.newPlainText("LeanType theme", getColorString(prefs, newThemeName.text))) + cm.setPrimaryClip(ClipData.newPlainText("LeanTypeDual theme", getColorString(prefs, newThemeName.text))) }, ), onClickBack = onClickBack, diff --git a/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt index c237e77ba..73b86009e 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt @@ -68,6 +68,8 @@ fun GestureTypingScreen( add(Settings.PREF_SPACE_HORIZONTAL_SWIPE) add(Settings.PREF_SPACE_VERTICAL_SWIPE) add(Settings.PREF_DELETE_SWIPE) + add(Settings.PREF_SWIPE_UP_MENU) + add(Settings.PREF_SWIPE_DOWN_MENU) add(R.string.settings_category_touchpad) add(Settings.PREF_TOUCHPAD_SENSITIVITY) @@ -175,6 +177,12 @@ fun createGestureTypingSettings(context: Context) = listOf( Setting(context, Settings.PREF_DELETE_SWIPE, R.string.delete_swipe, R.string.delete_swipe_summary) { SwitchPreference(it, Defaults.PREF_DELETE_SWIPE) }, + Setting(context, Settings.PREF_SWIPE_UP_MENU, R.string.swipe_up_menu, R.string.swipe_up_menu_summary) { + SwitchPreference(it, Defaults.PREF_SWIPE_UP_MENU) + }, + Setting(context, Settings.PREF_SWIPE_DOWN_MENU, R.string.swipe_down_menu, R.string.swipe_down_menu_summary) { + SwitchPreference(it, Defaults.PREF_SWIPE_DOWN_MENU) + }, ) @Preview diff --git a/app/src/main/java/helium314/keyboard/settings/screens/UpdatesScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/UpdatesScreen.kt index bbe4c03a7..b19da1077 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/UpdatesScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/UpdatesScreen.kt @@ -104,7 +104,7 @@ fun UpdatesScreen( try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!context.packageManager.canRequestPackageInstalls()) { - Toast.makeText(context, "Please allow LeanType to install unknown apps", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Please allow LeanTypeDual to install unknown apps", Toast.LENGTH_LONG).show() val permissionIntent = Intent( AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}") @@ -139,7 +139,7 @@ fun UpdatesScreen( try { val updatesDir = File(context.cacheDir, "updates") if (!updatesDir.exists()) updatesDir.mkdirs() - val targetFile = File(updatesDir, "LeanType_${versionTag}.apk") + val targetFile = File(updatesDir, "LeanTypeDual_${versionTag}.apk") var url = URL(apkUrl) var conn = url.openConnection() as HttpURLConnection diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 9ee350736..9346afbb8 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -101,8 +101,8 @@ Indien gedeaktiveerd, sal knipbord sleutel plak as dit inhoud het Geskiedenis behou tyd Sleutelgrense - LeanType Spell Checker - LeanType Spell Checker Settings + LeanTypeDual Spell Checker + LeanTypeDual Spell Checker Settings Nommers ry Wys altyd die nommers ry Wys sleutel wenke @@ -119,7 +119,7 @@ Lang-druk van spasiebalk sal toevoer metode seleksie skerm wys Outo-spasie na leestekens Plaas \'n spasie na \'n leesteken sodra \'n nuwe woord getik word - LeanType Instellings + LeanTypeDual Instellings Invoer Bykomende Sleutels Regstellings diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c870ffa4d..99d48d0f1 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -93,7 +93,7 @@ "مزيد من اللغات…" مفتاح الرموز التعبيرية بلا حدود - إعدادات LeanType + إعدادات LeanTypeDual الإدخال تعطيل تعلم الكلمات الجديدة المزيد من المفاتيح @@ -103,8 +103,8 @@ الاقتراحات تجريبي متنوعة - المدقق الإملائي لـ LeanType - إعدادات المدقق الإملائي لـ LeanType + المدقق الإملائي لـ LeanTypeDual + إعدادات المدقق الإملائي لـ LeanTypeDual صف الأرقام أظهِر صف الأرقام دائما سيتبع المظهر إعدادات النظام @@ -498,7 +498,7 @@ يُرجى التبديل إلى %s لتجنب تجاوز حدود الحجم، سيتم تصدير أول %s كلمة فقط - LeanType + LeanTypeDual الإعدادات المرئيات والمعاينة السلوك @@ -621,7 +621,7 @@ قفل داخلي: يمنع جميع طلبات الشبكة من هذا التطبيق. لإلغاء الإذن تمامًا، استخدم إعدادات معلومات التطبيق في النظام. فتح معلومات تطبيق النظام إدارة الأذونات واستخدام البطارية في إعدادات Android - تم تعطيل الوصول إلى الشبكة في إعدادات LeanType + تم تعطيل الوصول إلى الشبكة في إعدادات LeanTypeDual مزوّد AI اختيار خدمة AI للتدقيق Google Gemini diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index a54ffe6e6..2e5fa616a 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -92,11 +92,11 @@ "Bütün dillər üçün" "Digər dillər…" Pano keçmişi - LeanType Yazı Yoxlayıcısı - LeanType Yazı Yoxlayıcısı Parametrləri + LeanTypeDual Yazı Yoxlayıcısı + LeanTypeDual Yazı Yoxlayıcısı Parametrləri "Daxiletmə metodunu seçin" "Görünüş" - LeanType Tənzimləmələri + LeanTypeDual Tənzimləmələri Giriş Əlavə Düymələr "Ləğv edin" diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 15e8c60b4..986d1ec97 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -99,12 +99,12 @@ Рознае Без абмежаванняў %s хв. - Налады праверкі правапісу LeanType + Налады праверкі правапісу LeanTypeDual Клавіша эмодзі - Налады LeanType + Налады LeanTypeDual Ўвод Дадатковыя клавішы - Праверка правапісу LeanType + Праверка правапісу LeanTypeDual Ўключыць гісторыю буфера абмену Калі выключана, клавіша буфера абмену будзе ўстаўляць змесціва буфера абмену, калі яно ёсць Час захоўвання гісторыі буфера абмену diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 35312fac3..ac4345751 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -109,9 +109,9 @@ Предложения Експериментални Разни - Проверка на правописа - Настройки - Настройки за проверка на правописа + LeanTypeDual Проверка на правописа + LeanTypeDual Настройки + LeanTypeDual Настройки за проверка на правописа Превключване на езика Допълнителни бутони Бутон за емотикони diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 96097dc92..7925337b3 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -4,9 +4,9 @@ modified SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only --> - হেলিবোর্ড বানান পরীক্ষক - হেলিবোর্ড সেটিংস - হেলিবোর্ড বানান পরীক্ষক সেটিংস + LeanTypeDual বানান পরীক্ষক + LeanTypeDual সেটিংস + LeanTypeDual বানান পরীক্ষক সেটিংস পরিচিতি তালিকায় নাম অনুসন্ধান কিপ্রেস কম্পন কিপ্রেসের শব্দ diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 1a5a250e0..4c21ce2cf 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -98,14 +98,14 @@ Llisqueu damunt de la tecla d\'esborrar per seleccionar i treure grans parts de text d\'un sol cop Força el mode d\'incògnit %s min - Corrector ortogràfic de l\'LeanType - Configuració del corrector ortogràfic de l\'LeanType + Corrector ortogràfic de l\'LeanTypeDual + Configuració del corrector ortogràfic de l\'LeanTypeDual Esborra amb lliscament Tecla d\'accés a emojis Desactiva l\'aprenentatge de paraules noves Fila de nombres Mostra sempre la fila de nombres - Configuració de l\'LeanType + Configuració de l\'LeanTypeDual Entrada Miscel·lània Vores del teclat diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 345cf5259..3e1805696 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -101,8 +101,8 @@ Bez limitu Povolit historii schránky Doba zachování historie - Kontrola pravopisu LeanType - Nastavení kontroly pravopisu LeanType + Kontrola pravopisu LeanTypeDual + Nastavení kontroly pravopisu LeanTypeDual Automaticky vloží mezeru po ukončení slova znaménkem Vynutit režim inkognito Vypnout učení nových slov @@ -117,7 +117,7 @@ Dlouhý stisk mezerníku zobrazí výzvu změny vstupní metody Měřítko výšky klávesnice Vzhled podle systémového nastavení - Nastavení LeanType + Nastavení LeanTypeDual Návrhy Klávesa pro emodži Pokud vypnuto, tlačítko schránky vloží obsah schránky, pokud nějaký existuje diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index e2f84b550..671183e74 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -103,7 +103,7 @@ Automatisk dag-/nattilstand Udseende følger systemindstillinger %s min. - LeanType-stavekontrol + LeanTypeDual-stavekontrol Vis tastehints Talrække Vis altid talrækken @@ -111,11 +111,11 @@ Emoji-tast Skift inputmetode med mellemrumstasten Langt tryk på mellemrumstasten åbner menuen til valg af inputmetode - Indstillinger for LeanType-stavekontrol + Indstillinger for LeanTypeDual-stavekontrol Indsæt automatisk mellemrum efter skilletegn, når du skriver et nyt ord Auto-mellemrum efter skilletegn %s (Akkhor) - LeanType-indstillinger + LeanTypeDual-indstillinger Input Yderligere taster Diverse diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3557dacf4..475b01f1e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -95,12 +95,12 @@ Zwischenablage-Verlauf Korrekturen Vorschläge - LeanType-Rechtschreibprüfungs-Einstellungen - LeanType-Einstellungen + LeanTypeDual-Rechtschreibprüfungs-Einstellungen + LeanTypeDual-Einstellungen Experimentell Keine Begrenzung %s Min. - LeanType-Rechtschreibprüfung + LeanTypeDual-Rechtschreibprüfung Emoji-Taste Eingabe Zusätzliche Tasten @@ -443,7 +443,7 @@ %s (Khipro) Funktionale Tasten (Khipro) - LeanType + LeanTypeDual Konfiguration Optik & Vorschau Verhalten @@ -567,7 +567,7 @@ Interne Sperre: Blockiert alle Netzwerkanfragen dieser App. Um die Berechtigung vollständig zu entziehen, nutzen Sie die System-App-Info-Einstellungen. System-App-Info öffnen Berechtigungen und Akkunutzung in den Android-Einstellungen verwalten - Netzwerkzugriff ist in den LeanType-Einstellungen deaktiviert + Netzwerkzugriff ist in den LeanTypeDual-Einstellungen deaktiviert AI-Anbieter Wählen Sie den AI-Dienst zum Korrekturlesen Google Gemini diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index ad55fb126..c3f1ca8a2 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -97,7 +97,7 @@ Ενεργοποίηση ιστορικού προχείρου Χρόνος διατήρησης ιστορικού Πατώντας παρατεταμένο το πλήκτρο διαστήματος θα εμφανιστεί το μενού επιλογής μεθόδου εισαγωγής - Ορθογραφικός Έλεγχος LeanType + Ορθογραφικός Έλεγχος LeanTypeDual Διαγραφή ολίσθησης Πραγματοποιήστε μια σάρωση από το πλήκτρο διαγραφής για να επιλέξετε και να αφαιρέσετε μεγαλύτερα τμήματα κειμένου ταυτόχρονα Αναγκαστική λειτουργία ανώνυμης περιήγησης @@ -105,7 +105,7 @@ Πρόσθετα κλειδιά Διορθώσεις Προτάσεις - Ρυθμίσεις ορθογραφικού ελέγχου LeanType + Ρυθμίσεις ορθογραφικού ελέγχου LeanTypeDual Ιστορικό πρόχειρου Διάφορα %smin. @@ -120,7 +120,7 @@ %s (Akkhor) Η εμφάνιση θα ακολουθεί τις ρυθμίσεις του συστήματος Αυτόματο κενό μετά τη στίξη - Ρυθμίσεις LeanType + Ρυθμίσεις LeanTypeDual Εισαγωγή Περισσότερα πλήκτρα Απενεργοποιήστε την εκμάθηση νέων λέξεων @@ -140,7 +140,7 @@ "Παύση" "Αναμονή" - LeanType + LeanTypeDual Δόνηση σε λειτουργία μην ενοχλείτε Πρόταση emoji Χρήση λεξικού emoji στις κανονικές προτάσεις @@ -375,7 +375,7 @@ Εσωτερικό κλείδωμα: Αποκλείει όλα τα αιτήματα δικτύου από αυτήν την εφαρμογή. Για πλήρη ανάκληση της άδειας, χρησιμοποιήστε τις ρυθμίσεις Πληροφοριών εφαρμογής συστήματος. Άνοιγμα Πληροφοριών εφαρμογής συστήματος Διαχείριση αδειών και χρήσης μπαταρίας στις ρυθμίσεις Android - Η πρόσβαση δικτύου είναι απενεργοποιημένη στις ρυθμίσεις του LeanType + Η πρόσβαση δικτύου είναι απενεργοποιημένη στις ρυθμίσεις του LeanTypeDual Πάροχος AI Επιλέξτε υπηρεσία AI για διόρθωση κειμένου Google Gemini diff --git a/app/src/main/res/values-en-rAU/strings.xml b/app/src/main/res/values-en-rAU/strings.xml index 02b73a469..49e6253e4 100644 --- a/app/src/main/res/values-en-rAU/strings.xml +++ b/app/src/main/res/values-en-rAU/strings.xml @@ -104,9 +104,9 @@ "Search" "Pause" "Wait" - LeanType Spell Checker - LeanType Settings - LeanType Spell Checker Settings + LeanTypeDual Spell Checker + LeanTypeDual Settings + LeanTypeDual Spell Checker Settings Split distance Emoji key Switch Language diff --git a/app/src/main/res/values-en-rCA/strings.xml b/app/src/main/res/values-en-rCA/strings.xml index fc6ea140d..1322584f1 100644 --- a/app/src/main/res/values-en-rCA/strings.xml +++ b/app/src/main/res/values-en-rCA/strings.xml @@ -104,9 +104,9 @@ "Search" "Pause" "Wait" - LeanType Spell Checker - LeanType Settings - LeanType Spell Checker Settings + LeanTypeDual Spell Checker + LeanTypeDual Settings + LeanTypeDual Spell Checker Settings Vibrate in do not disturb mode Suggest emojis Use emoji dictionary in normal suggestions diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 2d896f896..baa2579f5 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -92,8 +92,8 @@ "Edit word" "For all languages" "More languages…" - LeanType Spell Checker - LeanType Spell Checker Settings + LeanTypeDual Spell Checker + LeanTypeDual Spell Checker Settings Always show number row Delete swipe Disable learning of new words @@ -105,7 +105,7 @@ Experimental Input Miscellaneous - LeanType Settings + LeanTypeDual Settings Suggestions Number row Show key hints diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 14d4d67f9..9b9dac112 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -94,8 +94,8 @@ Habilitar historial del portapapeles Tiempo para mantener el historial %smin. - Corrector ortográfico de LeanType - Ajustes del corrector ortográfico de LeanType + Corrector ortográfico de LeanTypeDual + Ajustes del corrector ortográfico de LeanTypeDual Escala de altura del teclado Tecla de emoji Si está deshabilitada, la tecla del portapapeles pegará el contenido del portapapeles si lo hay @@ -120,7 +120,7 @@ Bordes de teclas Seguir la configuración del sistema Modo claro/oscuro automático - Ajustes de LeanType + Ajustes de LeanTypeDual Entrada Teclas adicionales Correcciones @@ -369,7 +369,7 @@ Intro Espacio (diseño numérico) - LeanType + LeanTypeDual Vibrar en modo no molestar Sugerir emojis Usar diccionario de emojis en las sugerencias normales @@ -533,7 +533,7 @@ Bloqueo interno: Bloquea todas las solicitudes de red de esta app. Para revocar el permiso completamente, usa los ajustes de Información de la app del sistema. Abrir Información de la app del sistema Administrar permisos y uso de batería en los ajustes de Android - El acceso a red está desactivado en los ajustes de LeanType + El acceso a red está desactivado en los ajustes de LeanTypeDual Proveedor de AI Selecciona el servicio AI para la revisión Google Gemini diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 836cb00f3..c5297e09b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -96,8 +96,8 @@ Teclas adicionales Cambiar método de entrada con la barra espaciadora Al presionar por mucho tiempo la barra espaciadora, aparecerá el menú de selección del método de entrada - Corrector ortográfico de LeanType - Ajustes del corrector ortográfico de LeanType + Corrector ortográfico de LeanTypeDual + Ajustes del corrector ortográfico de LeanTypeDual Escala de altura del teclado Borrar deslizando Deslice el dedo por la tecla Borrar para seleccionar y eliminar porciones más grandes de texto de una vez @@ -108,7 +108,7 @@ Correcciones Experimental Entrada - Ajustes de LeanType + Ajustes de LeanTypeDual Sugerencias Otros Fila de números diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 2bf866976..a5f5da56b 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -109,9 +109,9 @@ Lisaklahvid Vaheta sisestusmeetodit tühikuklahviga Seadista klahvistikku - LeanTypei õigekirjakontroll - LeanTypei seadistused - LeanTypei õigekirjakontrolli seadistused + LeanTypeDuali õigekirjakontroll + LeanTypeDuali seadistused + LeanTypeDuali õigekirjakontrolli seadistused Klahvistiku taust Vastupidine suund, kui klahvistiku paremalt-vasakule alamtüüp on valitud Klahvistiku kõrguse proportsioonid diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 590f33be1..66f74bcbe 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -108,7 +108,7 @@ Tekla gehiago Zenbakien errenkada Zuriune tekla luze sakatzeak sarrera-metodoa hautatzeko menua erakutsiko du - LeanType ortografia zuzentzailea + LeanTypeDual ortografia zuzentzailea Zuriunea puntuazioen ondoren Behartu ezkutuko modua %s (Akkhor hizkuntza) @@ -116,9 +116,9 @@ Teklen ertzak Egun/gau modu automatikoa Itxurak sistemaren ezarpenei jarraituko die - LeanType ezarpenak + LeanTypeDual ezarpenak Tekla gehigarriak - LeanType ortografia zuzentzailearen ezarpenak + LeanTypeDual ortografia zuzentzailearen ezarpenak Emoji tekla Desgaitu irristatzea Ezabatu teklatik hatza irristatu, testuaren zati handiagoak hautatzeko eta ezabatzeko diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 9c9dc5226..d717c2233 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -95,10 +95,10 @@ فعال‌کردن تاریخچهٔ تخته‌گیره "انتخاب روش ورودی" تاریخچهٔ تخته‌گیره - غلط‌یاب املایی LeanType - تنظیمات غلط‌یاب املایی LeanType + غلط‌یاب املایی LeanTypeDual + تنظیمات غلط‌یاب املایی LeanTypeDual دکمهٔ شکلک - تنظیمات LeanType + تنظیمات LeanTypeDual کلیدهای افزوده آزمایشی متفرقه diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 83940e041..9b4d4af21 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -162,7 +162,7 @@ Vaalea Näytä työkalupalkki jos syöte aloitetaan tai tekstiä valitaan Numeronäppäimistö päällä/pois - LeanType-oikoluvun asetukset + LeanTypeDual-oikoluvun asetukset Tietoja Näytä GitHubissa Versio @@ -233,10 +233,10 @@ Välilyönnin tausta Näytä mahdollisesti huomaamatta jäävät ominaisuudet Työkalupalkki - LeanType-oikoluku + LeanTypeDual-oikoluku Kuvaus piilotetuista ominaisuuksista Valitse työkalupalkin näppäimet - LeanType-asetukset + LeanTypeDual-asetukset Dynaamiset värit Ei rajoitusta Valitse kiinnitetyt työkalupalkin näppäimet diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 8d90f4ff4..863d06a2b 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,6 +1,6 @@ - Mga Setting ng LeanType + Mga Setting ng LeanTypeDual Mag-vibrate sa keypress Tunog sa keypress Mag-popup sa keypress @@ -46,8 +46,8 @@ Off Mababang-loob Agresibo - Spell Checker ng LeanType - Mga Setting ng Spell Checker ng LeanType + Spell Checker ng LeanTypeDual + Mga Setting ng Spell Checker ng LeanTypeDual Hanapin ang mga pangalan ng contact Mga mungkahi %s ms diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 686fc3513..fce356cfe 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -134,8 +134,8 @@ Nouveau dictionnaire: "Plus de langues…" Ce mot est déjà présent dans le dictionnaire utilisateur %s. Veuillez en saisir un autre. Ajouter un mot - Correcteur orthographique LeanType - Paramètres du correcteur orthographique LeanType + Correcteur orthographique LeanTypeDual + Paramètres du correcteur orthographique LeanTypeDual Ligne de chiffres Balayage sur la touche Effacer Glissez sur la touche de suppression pour supprimer des parties de texte en une seule fois @@ -153,7 +153,7 @@ Nouveau dictionnaire: Fichier de bibliothèque inconnu. Êtes-vous sûr qu\'il provient d\'une source fiable et que c\'est pour CPU \'%s\' ? Charger bibliothèque Supprimer bibliothèque - Paramètres d\'LeanType + Paramètres d\'LeanTypeDual Saisie Corrections Suggestions @@ -495,7 +495,7 @@ Nouveau dictionnaire: Pour éviter de dépasser les limites de taille, seuls les %s premiers mots seront exportés Informations sur la collecte de données gestuelles sur : - LeanType + LeanTypeDual Configuration Visuels & Aperçu Comportement @@ -619,7 +619,7 @@ Nouveau dictionnaire: Verrou interne : bloque toutes les requêtes réseau de cette application. Pour révoquer complètement l\'autorisation, utilisez les paramètres d\'informations sur l\'application du système. Ouvrir les infos de l\'application système Gérer les autorisations et l\'utilisation de la batterie dans les paramètres Android - L\'accès réseau est désactivé dans les paramètres de LeanType + L\'accès réseau est désactivé dans les paramètres de LeanTypeDual Fournisseur AI Sélectionnez le service AI pour la relecture Google Gemini diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 4c7e326c8..dd6e49a11 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -258,7 +258,7 @@ Esta palabra xa está presente no dicionario %s da usuaria. Por favor escribe outra. Negro Índigo - Axustes de LeanType + Axustes de LeanTypeDual Preferir números traducidos sobre os latinos Tes certeza de querer substituír o dicionario engadido \"%1$s\"? \n @@ -269,8 +269,8 @@ \n%3$s aquí Fondo da tecla función - Revisor de Ortografía - Axustes do revisor de Ortografía de LeanType + LeanTypeDual Revisor de Ortografía + Axustes do revisor de Ortografía de LeanTypeDual Vas precisar a biblioteca para \'%s\': As bibliotecas incompatibles podería estragar a app ao escribir. \n \nAviso: a carga de código externo é un risco de seguridade. Usa só bibliotecas de fontes de confianza. diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index d9b9d1a50..39583e410 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -91,8 +91,8 @@ "Uređivanje riječi" "Za sve jezike" "Više jezika…" - LeanType provjera pravopisa - Postavke LeanType provjere pravopisa + LeanTypeDual provjera pravopisa + Postavke LeanTypeDual provjere pravopisa %s (Akkhor) Prijedlozi Povijest međuspremnika @@ -102,7 +102,7 @@ Tipka za emojije %s min Bez ograničenja - LeanType postavke + LeanTypeDual postavke Unos Dodatne tipke Omogući povijest međuspremnika diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 21a374393..cafd687c1 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -99,8 +99,8 @@ Korlátlan Vágólap előzményeinek engedélyezése Betűhatárok - LeanType beállítások - LeanType helyesírás-ellenőrző beállításai + LeanTypeDual beállítások + LeanTypeDual helyesírás-ellenőrző beállításai További billentyűk A törlés billentyűről való csúsztatással nagyobb szövegrészleteket jelölhet ki és távolíthat el Ha le van tiltva, a vágólapbillentyű beilleszti a vágólap tartalmát, ha van @@ -112,7 +112,7 @@ A szóközbillentyű nyomva tartásával a beviteli mód kiválasztási menüje jelenik meg Beviteli mód megváltoztatása a szóközzel Automatikus nappali/éjszakai mód - LeanType helyesírás-ellenőrző + LeanTypeDual helyesírás-ellenőrző Törlés csúsztatással Emodzsibillentyű Inkognitómód kényszerítése diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index adcac5542..481348530 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -101,13 +101,13 @@ Tampilkan petunjuk tombol Selalu tampilkan baris angka %s mnt - Pemeriksa Ejaan LeanType - Pengaturan Pemeriksa Ejaan LeanType + Pemeriksa Ejaan LeanTypeDual + Pengaturan Pemeriksa Ejaan LeanTypeDual Baris angka Ubah metode masukan dengan tombol spasi Paksa mode privat Tombol emoji - Pengaturan LeanType + Pengaturan LeanTypeDual Masukan Tombol tambahan Saran diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 167acd5d1..265f1700d 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -113,8 +113,8 @@ Litir Rúnnað Stíll - Stafsetningarleiðrétting LeanType - Stillingar LeanType + Stafsetningarleiðrétting LeanTypeDual + Stillingar LeanTypeDual Öryggisafrit Hlaða inn orðasafni Eyða orðasafni @@ -124,7 +124,7 @@ Veldu tungumál Skipta út orðasafni Veldu orð - Stillingar stafsetningarleiðréttingar LeanType + Stillingar stafsetningarleiðréttingar LeanTypeDual Innsláttur Viðbótarlyklar Ferilskrá klippispjalds diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d48d9335f..ea34c966d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -101,8 +101,8 @@ Attiva la cronologia appunti Se disattivata, il tasto \'Appunti\' incolla direttamente l\'ultimo contenuto Durata dei contenuti nella cronologia - Controllo ortografico LeanType - Impostazioni del controllo ortografico LeanType + Controllo ortografico LeanTypeDual + Impostazioni del controllo ortografico LeanTypeDual Riga dei numeri Scorri per cancellare Scorri indietro partendo da \'Backspace\' per rimuovere più caratteri o parole in una sola volta @@ -121,7 +121,7 @@ Opzioni sperimentali Varie Input - Impostazioni di LeanType + Impostazioni di LeanTypeDual Disattiva l\'apprendimento di nuove parole Bordi dei tasti Tema chiaro/scuro automatico @@ -497,7 +497,7 @@ Prego cambiare a %s Per non superare i limiti di dimensione, verranno esportate solo le prime %s parole - LeanType + LeanTypeDual Configurazione Aspetto e anteprima Comportamento @@ -621,7 +621,7 @@ Blocco interno: blocca tutte le richieste di rete da questa app. Per revocare completamente il permesso, usa le impostazioni di sistema Info app. Apri Info app di sistema Gestisci permessi e utilizzo batteria nelle impostazioni Android - L\'accesso alla rete è disabilitato nelle impostazioni di LeanType + L\'accesso alla rete è disabilitato nelle impostazioni di LeanTypeDual Provider AI Seleziona il servizio AI per la correzione bozze Google Gemini diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index c4f09322d..93840baec 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -104,16 +104,16 @@ קנה-מידה גובה המקלדת גבולות מקלדת מצב יום/לילה אוטומטי - הגדרות בודק האיות של LeanType + הגדרות בודק האיות של LeanTypeDual שינוי שיטת ההזנה על-ידי מקש רווח מקש אמוג\'י %s (תווים) - בודק האיות של LeanType + בודק האיות של LeanTypeDual היסטוריית הלוח הוספת רווח אוטומטית אחרי סימני פיסוק המראה יעקוב אחר הגדרות המערכת הוספת רווח אוטומטית לאחר סימני פיסוק בעת הקלדת מילה חדשה - הגדרות LeanType + הגדרות LeanTypeDual קֶלֶט נסיוני שונות diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index db891ab91..5a2cd196d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -103,14 +103,14 @@ "検索" "停止" "待機" - ヘリボードの設定 + LeanTypeDualの設定 遅延無し - ヘリボードスペルチェッカ + LeanTypeDualスペルチェッカ 入力 %s 分 退避とレストア スプリットキーボードをイネーブルする(横向き) - ヘリボードスペルチェッカの設定 + LeanTypeDualスペルチェッカの設定 提案 実験的 注意:この設定をディセーブルしたら習ったデータが消える diff --git a/app/src/main/res/values-jpr/strings.xml b/app/src/main/res/values-jpr/strings.xml index 49d85fb93..920108219 100644 --- a/app/src/main/res/values-jpr/strings.xml +++ b/app/src/main/res/values-jpr/strings.xml @@ -4,7 +4,7 @@ חדֿףֿ בזרגכרדן כֿודכאר חרוףֿ תצחיח כֿודכאר - מנטׄימאת LeanType‬‏ + מנטׄימאת LeanTypeDual‬‏ פישנהאדאת שכֿצי שודה מנטׄימאת מיאנבר:‏ diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 1541d4204..85d337049 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -112,11 +112,11 @@ გამოტოვების ღილაკზე დიდხანს დაწოლა შეყვანის მეთოდის მენიუს გაჩვენებთ ღილაკის მინიშნებების ჩვენება დიდხანს-დაწოლის მინიშენების ჩვენება - LeanType-ის მართლწერის შემოწმება - LeanType-ის მართლწერის შემოწმების მორგება + LeanTypeDual-ის მართლწერის შემოწმება + LeanTypeDual-ის მართლწერის შემოწმების მორგება ემოჯის ღილაკი ჟესტით წაშლა - LeanType-ის მორგება + LeanTypeDual-ის მორგება შეყვანა დამატებითი ღილაკები მინიშნებები diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index a35b3ccb3..bad571748 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -5,7 +5,7 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only --> Imenyafen - Iɣewwaren n LeanType + Iɣewwaren n LeanTypeDual Wiyyaḍ Tutlayt : Beddel awal-a diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 9c9a09d7b..27518cc28 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -94,9 +94,9 @@ 마침표 후 자동 띄어쓰기 새로운 단어를 입력할 때 마침표가 앞에 있다면 자동으로 띄어쓰기 하기 "입력 방법 선택" - LeanType 맞춤법 검사기 - LeanType 맞춤법 검사기 설정 - LeanType 설정 + LeanTypeDual 맞춤법 검사기 + LeanTypeDual 맞춤법 검사기 설정 + LeanTypeDual 설정 새로운 단어를 배우는 것이 불가능 자동 교정 정확도 추천 diff --git a/app/src/main/res/values-kw/strings.xml b/app/src/main/res/values-kw/strings.xml index 76de02b76..62874fd9e 100644 --- a/app/src/main/res/values-kw/strings.xml +++ b/app/src/main/res/values-kw/strings.xml @@ -1,8 +1,8 @@ - Checkyer Lytherennans LeanType - Settyansow LeanType - Settyansow Checkyer Lytherennans LeanType + Checkyer Lytherennans LeanTypeDual + Settyansow LeanTypeDual + Settyansow Checkyer Lytherennans LeanTypeDual Krena war tava Krena yn fordh na ankombra Sowndya war tava diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 0322f7a18..3135a3067 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -105,9 +105,9 @@ "Paieška" Pristabdyti "Lauk." - LeanType rašybos tikrinimas - LeanType nustatymai - LeanType rašybos tikrinimo nustatymai + LeanTypeDual rašybos tikrinimas + LeanTypeDual nustatymai + LeanTypeDual rašybos tikrinimo nustatymai Vibruoti netrukdymo režime Siūlyti jaustukus Naudoti jaustukų žodyną įprastuose pasiūlymuose diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 1aeebca89..ee15c7ab9 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -103,9 +103,9 @@ "Meklēt" "Pauze" "Gaidīt" - LeanType pareizrakstības pārbaudītājs - LeanType iestatījumi - LeanType pareizrakstības pārbaudītāja iestatījumi + LeanTypeDual pareizrakstības pārbaudītājs + LeanTypeDual iestatījumi + LeanTypeDual pareizrakstības pārbaudītāja iestatījumi Ieteikumi Ievade Papildus taustiņi diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index cd49e06f6..b7dba5a80 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -99,13 +99,13 @@ എല്ലായ്പ്പോഴും നമ്പർ വരി കാണിക്കുക നമ്പർ വരി കീ സൂചനകൾ കാണിക്കുക - ഓപ്പൺബോർഡ് സ്പെൽ ചെക്കർ ക്രമീകരണങ്ങൾ + LeanTypeDual സ്പെൽ ചെക്കർ ക്രമീകരണങ്ങൾ ഡിലീറ്റ് സ്വൈപ്പ് ഇമോജി കീ പുതിയ വാക്കുകൾ പഠിക്കുന്നത് അപ്രാപ്തമാക്കുക നിർദ്ദേശങ്ങൾ - ഓപ്പൺബോർഡ് സ്പെൽ ചെക്കർ - ഓപ്പൺബോർഡ് ക്രമീകരണങ്ങൾ + LeanTypeDual സ്പെൽ ചെക്കർ + LeanTypeDual ക്രമീകരണങ്ങൾ ക്ലിപ്പ്ബോർഡ് ഹിസ്റ്ററി പ്രവർത്തനരഹിതമാക്കിയാൽ, ക്ലിപ്പ്ബോർഡ് ഉള്ളടക്കം എന്തെങ്കിലും ഉണ്ടെങ്കിൽ ക്ലിപ്പ്ബോർഡ് കീ പേസ്റ്റ് ചെയ്യും ക്ലിപ്പ്ബോർഡ് ചരിത്രം എനേബിൾ ചെയ്യുക @@ -139,7 +139,7 @@ "താൽക്കാലികമായി നിർത്തുക" "കാത്തിരിക്കുക" - LeanType + LeanTypeDual ശല്യം ചെയ്യാതിരിക്കുക മോഡിൽ വൈബ്രേറ്റ് ചെയ്യുക ഇമോജികൾ നിർദ്ദേശിക്കുക സാധാരണ നിർദ്ദേശങ്ങളിൽ ഇമോജി നിഘണ്ടു ഉപയോഗിക്കുക @@ -374,7 +374,7 @@ ആന്തരിക ലോക്ക്: ഈ ആപ്പിൽ നിന്നുള്ള എല്ലാ നെറ്റ്വർക്ക് അഭ്യർത്ഥനകളും തടയുന്നു. അനുമതി പൂർണ്ണമായും റദ്ദാക്കാൻ, സിസ്റ്റം ആപ്പ് വിവര ക്രമീകരണങ്ങൾ ഉപയോഗിക്കുക. സിസ്റ്റം ആപ്പ് വിവരം തുറക്കുക Android ക്രമീകരണങ്ങളിൽ അനുമതികളും ബാറ്ററി ഉപയോഗവും നിയന്ത്രിക്കുക - LeanType ക്രമീകരണങ്ങളിൽ നെറ്റ്വർക്ക് ആക്സസ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു + LeanTypeDual ക്രമീകരണങ്ങളിൽ നെറ്റ്വർക്ക് ആക്സസ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു AI ദാതാവ് പ്രൂഫ് റീഡിംഗിനായി AI സേവനം തിരഞ്ഞെടുക്കുക Google Gemini diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index f7382d639..f78710ca7 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -104,9 +104,9 @@ "Carian" "Jeda" "Tggu" - Pemeriksa Ejaan LeanType - Tetapan LeanType - Tetapan Pemeriksa Ejaan LeanType + Pemeriksa Ejaan LeanTypeDual + Tetapan LeanTypeDual + Tetapan Pemeriksa Ejaan LeanTypeDual Getar dalam mod Jangan Kacau Cadangkan emoji Gunakan kamus emoji dalam cadangan normal diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 64133c02f..2dcb96409 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -103,9 +103,9 @@ "ရှာဖွေရန်" "ဆိုင်းငံ့ရန်" "စောင့်ဆိုင်းရန်" - ဟေလီဘုတ်ချိန်ညှိခြင်းများ - ဟေလီဘုတ်သတ်ပုံစစ် - ဟေလီဘုတ်သတ်ပုံစစ်ကို ချိန်ညှိရန် + LeanTypeDual ချိန်ညှိခြင်းများ + LeanTypeDual သတ်ပုံစစ် + LeanTypeDual သတ်ပုံစစ်ကို ချိန်ညှိရန် မနှောင့်ယှက်ရစနစ်ဖွင့်ထားချိန်တွင် တုန်ခါပါ အီမိုဂျီအကြံပေး အီမိုဂျီအဘိဓာန်ကို သာမန်အကြံပေးစနစ်တွင် အသုံးပြုပါ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index e37042a78..6523d0db7 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -95,9 +95,9 @@ Alltid vis tallraden Automatisk dag/nattmodus Langtrykke på mellomromstasten for inndatametode-valgmeny - LeanType-innstillinger - LeanType-stavekontroll - Innstillinger for LeanType-stavekontroll + LeanTypeDual-innstillinger + LeanTypeDual-stavekontroll + Innstillinger for LeanTypeDual-stavekontroll Slettingsdragning Emoji-tast Utfør en dragning fra Delete-tasten for å velge og fjerne større deler av tekst samtidig diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 414ba54cd..55fe4ad46 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -104,5 +104,5 @@ "खोजी गर्नुहोस्" "रोक्नुहोस्" "पर्खनुहोस्" - हेलीबोर्ड हिज्जे जाँचकर्ता + LeanTypeDual हिज्जे जाँचकर्ता diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index dad93c9a7..bcf569549 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -105,8 +105,8 @@ Meer toetsen Cijferregel altijd weergeven Veeg vanaf de delete-toets om grotere stukken tekst in één keer te selecteren en verwijderen - LeanType-spellingcontrole - LeanType-spellingcontrole-instellingen + LeanTypeDual-spellingcontrole + LeanTypeDual-spellingcontrole-instellingen Cijferregel Incognito-modus afdwingen Emojitoets @@ -124,7 +124,7 @@ Invoermethode schakelen met spatietoets Toetsranden Automatisch spatie invoegen na interpunctie en het typen van een nieuw woord - LeanType-instellingen + LeanTypeDual-instellingen Invoer Extra toetsen "Ongedaan maken" diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 4a48113f3..13a142e05 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -22,9 +22,9 @@ "ପ୍ରସ୍ତାବ ପ୍ରସ୍ତୁତ କରିବାରେ ପୂର୍ବବର୍ତ୍ତୀ ଶବ୍ଦ ବ୍ୟବହାର କରନ୍ତୁ" "ପୂର୍ବ ପରି କରନ୍ତୁ" "ପୁଣି କରନ୍ତୁ" - ହେଲିବୋର୍ଡ ସେଟିଂସ୍ - ହେଲିବୋର୍ଡ ବନାନ ଯାଞ୍ଚକାରୀ ସେଟିଂସ୍ - ହେଲିବୋର୍ଡ ବନାନ ଯାଞ୍ଚକାରୀ + LeanTypeDual ସେଟିଂସ୍ + LeanTypeDual ବନାନ ଯାଞ୍ଚକାରୀ ସେଟିଂସ୍ + LeanTypeDual ବନାନ ଯାଞ୍ଚକାରୀ ଇମୋଜ୍ଜି ସଙ୍କେତ କରିବା ଉଗ୍ର ଅତୀ ଉଗ୍ର diff --git a/app/src/main/res/values-ota/strings.xml b/app/src/main/res/values-ota/strings.xml index 36120c67d..40af87b7d 100644 --- a/app/src/main/res/values-ota/strings.xml +++ b/app/src/main/res/values-ota/strings.xml @@ -19,9 +19,9 @@ یدكلەمە سمبوللر اموجی - ‫LeanType‬ یازم دنەتلەییجیسی - ‫LeanType‬ عیارلری - ‫LeanType‬ یازم دنەتلەییجیسی عیارلری + ‫LeanTypeDual‬ یازم دنەتلەییجیسی + ‫LeanTypeDual‬ عیارلری + ‫LeanTypeDual‬ یازم دنەتلەییجیسی عیارلری كیشی آدلرڭی دنەتلە حركتلە یازمە دوزن diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml index bb17a4754..3dbe04bfb 100644 --- a/app/src/main/res/values-pa-rPK/strings.xml +++ b/app/src/main/res/values-pa-rPK/strings.xml @@ -12,7 +12,7 @@ ترجیحاں اضافی ہور چوݨاں - ‫LeanType‬ سیٹنگاں + ‫LeanTypeDual‬ سیٹنگاں لکھت نوں ٹھیک کرن ہور کنجیاں کاپی پیسٹ تریخ @@ -60,7 +60,7 @@ چوہݨ توں آواز کالا نھیرے توں نھیرا - ‫LeanType لفظ جوڑ دیاں سیٹنگاں‬ + ‫LeanTypeDual لفظ جوڑ دیاں سیٹنگاں‬ سپینی (امریکی) انگریزی (امریکی) بحال کرو @@ -100,6 +100,6 @@ کنجی دباوݨ تے پوپ‌اپ کرو دپھاڑ دا کی‌بورڈ نوں چالو کرو دپھاڑ دا فاصلا - ‫LeanType‬ لفظ جوڑن والا + ‫LeanTypeDual‬ لفظ جوڑن والا بولی بدلو diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6ac6b28aa..69df9b3ba 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -100,8 +100,8 @@ Więcej klawiszy Rząd numeryczny Klawisz emotikonów - Sprawdzanie pisowni LeanType - Ustawienia sprawdzania pisowni LeanType + Sprawdzanie pisowni LeanTypeDual + Ustawienia sprawdzania pisowni LeanTypeDual Historia schowka Włącz historię schowka Wymuś tryb incognito @@ -116,7 +116,7 @@ Pokazuj wskazówki klawiszy Skala wysokości klawiatury %s (Akkhor) - Ustawienia LeanType + Ustawienia LeanTypeDual Dodatkowe klawisze Wprowadzanie Sugestie diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 824e453a9..a29f87a2b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -4,7 +4,7 @@ modified SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only --> - Configurações do LeanType + Configurações do LeanTypeDual Vibrar ao tocar nas teclas Som ao tocar nas teclas Digitação por gestos @@ -81,8 +81,8 @@ Para todos os idiomas Mais idiomas… Bordas nas teclas - Verificador ortográfico do LeanType - Configurações do verificador ortográfico do LeanType + Verificador ortográfico do LeanTypeDual + Configurações do verificador ortográfico do LeanTypeDual Buscar por nomes de contatos Mostrar pop-up ao tocar em teclas Preferências diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index e3d9560a2..29f550243 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -99,8 +99,8 @@ Impor modo incógnito Modo diurno/noturno automático Aparência usa as definições do sistema - Corretor ortográfico LeanType - Definições de LeanType + Corretor ortográfico LeanTypeDual + Definições de LeanTypeDual Introdução Teclas adicionais Correções @@ -116,7 +116,7 @@ Toque longo na tecla de Espaço abre o menu de seleção do método de entrada Altura do teclado Limite das teclas - Definições do corretor ortográfico LeanType + Definições do corretor ortográfico LeanTypeDual Confiança da correção automática Se desativado, a tecla da área de transferência colará o conteúdo disponível "Escolher o método de entrada" diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6e34aa825..40b90d8c8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -91,8 +91,8 @@ "Editar palavra" "Para todos os idiomas" "Mais idiomas…" - Verificação ortográfica LeanType - Definições de verificação ortográfica LeanType + Verificação ortográfica LeanTypeDual + Definições de verificação ortográfica LeanTypeDual Número de linha Mostrar sempre n.º de linha Desativar aprendizagem de palavras @@ -102,7 +102,7 @@ Experimental Introdução Outras - Definições LeanType + Definições LeanTypeDual Sugestões Deslize para eliminar Mais teclas @@ -443,7 +443,7 @@ Definir fonte de emojis personalizada de ficheiro Teclas funcionais (Khipro) - LeanType + LeanTypeDual Configuração Visuais e pré-visualização Comportamento @@ -567,7 +567,7 @@ Bloqueio interno: Bloqueia todas as solicitações de rede desta app. Para revogar totalmente a permissão, use as definições de Informação da App do sistema. Abrir Informação da App do Sistema Gerir permissões e uso da bateria nas definições do Android - O acesso à rede está desativado nas definições do LeanType + O acesso à rede está desativado nas definições do LeanTypeDual Fornecedor de AI Selecionar serviço de AI para revisão Google Gemini diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b4d1eff58..94593ceda 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -104,7 +104,7 @@ Caută "Pauză" Așteaptă - Setări LeanType + Setări LeanTypeDual Chei suplimentare Corectări Sugestii @@ -122,8 +122,8 @@ Încrederea în corecție automată Timpul de păstrare a istoricului Introducere - Verificator ortografic LeanType - Setări pentru verificarea ortografică LeanType + Verificator ortografic LeanTypeDual + Setări pentru verificarea ortografică LeanTypeDual Istoric clipboard Activează istoricul clipboardului Avertisment: dezactivarea acestei setări va șterge datele învățate diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e8f813a93..cff245e71 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -119,10 +119,10 @@ Клавиша эмодзи История буфера обмена Включить журнал буфера обмена - Проверка орфографии LeanType - Настройки проверки орфографии LeanType + Проверка орфографии LeanTypeDual + Настройки проверки орфографии LeanTypeDual %s мин. - Настройки LeanType + Настройки LeanTypeDual Без ограничений Время хранения журнала буфера обмена %s (Акхор) @@ -604,7 +604,7 @@ Баскский - LeanType + LeanTypeDual Сенсорная панель Заблокированные слова Управление словами, заблокированными в предложениях @@ -710,7 +710,7 @@ Внутренняя блокировка: Блокирует все сетевые запросы от этого приложения. Чтобы полностью отозвать разрешение, используйте системные настройки приложения. Открыть системные настройки приложения Управляйте разрешениями и использованием батареи в настройках Android - Сетевой доступ отключен в настройках LeanType + Сетевой доступ отключен в настройках LeanTypeDual Выберите сервис AI для проверки орфографии Google Gemini Авто-растяжение кнопок панели инструментов diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 231cebde2..8b203be84 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -98,12 +98,12 @@ Povoliť históriu schránky Vypnúť učenie nových slov Viac kláves - LeanType Kontrola Pravopisu - Nastavenia kontroly pravopisu LeanType + LeanTypeDual Kontrola Pravopisu + Nastavenia kontroly pravopisu LeanTypeDual Emoji klávesa Vynútiť režim inkognito Vstup - LeanType Nastavenia + LeanTypeDual Nastavenia Dodatočné klávesy Korekcie Návrhy diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 735013cbf..1f950dfe4 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -109,12 +109,12 @@ Čas shranjevanja zgodovine Onemogočite, da boste s tipko odložišča lahko prilepili vsebino Več tipk - Nastavitve pregledovalnika črkovanja LeanType + Nastavitve pregledovalnika črkovanja LeanTypeDual Samodejni dnevni/nočni način Spremeni višino tipkovnice Poskusne možnosti - Pregledovalnik črkovanja LeanType - Nastavitve LeanType + Pregledovalnik črkovanja LeanTypeDual + Nastavitve LeanTypeDual Dodatne tipke Vnos Samodejni presledek za ločilom diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 3f834fca9..0e969f1f4 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -92,9 +92,9 @@ "За све језике" "Још језика…" "Избор метода уноса" - ХелиБоард провера правописа - ХелиБоард подешавања провере правописа - ХелиБоард подешавања + LeanTypeDual провера правописа + LeanTypeDual подешавања провере правописа + LeanTypeDual подешавања Унос Додатни кључеви Поузданост ауто-корекције diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4665a9e61..ee40da55f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -99,12 +99,12 @@ Ingen gräns Långtryckning på mellanslagstangenten öppnar menyn för val av inmatningsmetod Ändra inmatningsmetod med mellanslagstangenten - LeanTypes stavningskontroll - Inställningar för LeanTypes stavningskontroll + LeanTypeDuals stavningskontroll + Inställningar för LeanTypeDuals stavningskontroll Sifferrad Visa alltid sifferraden Emoji-tangent - LeanType-inställningar + LeanTypeDual-inställningar Visa tips för långtryckning Ytterligare tangenter Urklippshistorik diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 31d4aae12..5f318a282 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -103,9 +103,9 @@ "தேடு" "இடைநிறுத்து" "காத்திரு" - எலிபோர்டு எழுத்துப்பிழை சரிபார்ப்பு - எலிபோர்டு அமைப்புகள் - எலிபோர்டு எழுத்துப்பிழை சரிபார்ப்பு அமைப்புகள் + LeanTypeDual எழுத்துப்பிழை சரிபார்ப்பு + LeanTypeDual அமைப்புகள் + LeanTypeDual எழுத்துப்பிழை சரிபார்ப்பு அமைப்புகள் தொந்தரவு செய்யாதே பயன்முறையில் அதிர்வு ஈமோசிகளைப் பரிந்துரைக்கவும் சாதாரண பரிந்துரைகளில் ஈமோசி அகராதியைப் பயன்படுத்தவும் diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 3d5cfa464..b9aa60ae8 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -106,10 +106,10 @@ పరిమితి లేదు మరికొన్ని కీస్ సంఖ్యల వరుస - ఓపెన్ బోర్డ్ స్పెల్ల్ చెకర్ - ఓపెన్ బోర్డ్ స్పెల్ చెకర్ సెటింగ్స్ + LeanTypeDual స్పెల్ల్ చెకర్ + LeanTypeDual స్పెల్ చెకర్ సెటింగ్స్ కీ సూచనలు చూపండి - పరికరము యొక్క సాంకేతికతను బట్టి విద్యుత్ వినియోగము తగ్గించవచ్చు + LeanTypeDual పరికరము యొక్క సాంకేతికతను బట్టి విద్యుత్ వినియోగము తగ్గించవచ్చు అదనపు కీస్ "చర్య రద్దు చేయి" "చర్యను పునరావృతం చేయి" diff --git a/app/src/main/res/values-tg/strings.xml b/app/src/main/res/values-tg/strings.xml index f9fa767e2..15658cfdf 100644 --- a/app/src/main/res/values-tg/strings.xml +++ b/app/src/main/res/values-tg/strings.xml @@ -1,8 +1,8 @@ - Ғалатёби Имлоӣ LeanType - Танзимоти LeanType - Танзимоти Ғалатёби Имлоӣ LeanType + Ғалатёби Имлоӣ LeanTypeDual + Танзимоти LeanTypeDual + Танзимоти Ғалатёби Имлоӣ LeanTypeDual пайдо кардани иоми мухотабин Тарҳзамина \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b1edf1949..62edd864f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -96,12 +96,12 @@ Yeni sözcüklerin öğrenilmesini devre dışı bırak Sayı satırı Boşluk tuşuna uzun süre basmak, giriş yöntemi seçim menüsünü gösterecektir - LeanType yazım denetleyicisi ayarları + LeanTypeDual yazım denetleyicisi ayarları Klavye yükseklik ölçeği Kadırarak silme Tuş kenarlıkları Otomatik gündüz/gece modu - LeanType yazım denetleyicisi + LeanTypeDual yazım denetleyicisi %sdak. Öneriler Emoji tuşu @@ -116,7 +116,7 @@ %s (Akkhor) Bir kerede metnin daha büyük bölümlerini seçmek ve silmek için silme tuşundan hızlıca kaydırın Görünüm sistem ayarlarını takip edecektir - LeanType ayarları + LeanTypeDual ayarları Giriş Ek tuşlar Noktalama işaretlerinden sonra otomatik boşluk @@ -452,7 +452,7 @@ Aktif indirilmedi - LeanType + LeanTypeDual Yapılandırma Görseller ve Önizleme Davranış @@ -582,7 +582,7 @@ Uyarı: Harici kod yüklemek güvenlik riski oluşturabilir. Yalnızca güvendi Dahili kilit: Bu uygulamadan gelen tüm ağ isteklerini engeller. İzni tamamen iptal etmek için sistem Uygulama Bilgisi ayarlarını kullanın. Sistem Uygulama Bilgisini Aç Android ayarlarında izinleri ve pil kullanımını yönet - LeanType ayarlarında ağ erişimi devre dışı + LeanTypeDual ayarlarında ağ erişimi devre dışı Yapay Zeka Sağlayıcısı Yazım denetimi için AI hizmetini seçin Google Gemini diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index da1aba1c9..70d4e0425 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -92,7 +92,7 @@ Редагувати слово Для всіх мов "Інші мови…" - Налаштування LeanType + Налаштування LeanTypeDual Вигляд наслідує системні налаштування Автоматично вставляти пробіл після знаків пунктуації під час введення нового слова Стиль теми @@ -103,7 +103,7 @@ Введення %s (Аккгор) Автопробіл після знаків пунктуації - Налаштування перевірки правопису LeanType + Налаштування перевірки правопису LeanTypeDual Виправлення Пропозиції Експериментальне @@ -146,7 +146,7 @@ Показувати підказки клавіш Змінювати метод введення клавішею пробіл Довге натискання клавіші пробіл виводить меню вибору способу введення - Перевірка правопису LeanType + Перевірка правопису LeanTypeDual Збільшені клавіші Видаляти ковзанням Проведіть пальцем по клавіші видалення, щоб виділити і вилучити більші фрагменти тексту за раз diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index fe1305cb6..8dc60e474 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -104,10 +104,10 @@ "موقوف" "انتظار" - LeanType اسپیل چیکر - LeanType - LeanType ترتیبات - LeanType اسپیل چیکر کی ترتیبات + LeanTypeDual اسپیل چیکر + LeanTypeDual + LeanTypeDual ترتیبات + LeanTypeDual اسپیل چیکر کی ترتیبات پریشان نہ کریں موڈ میں وائبریٹ کریں ایموجیز تجویز کریں عام تجاویز میں ایموجی لغت استعمال کریں @@ -367,7 +367,7 @@ اندرونی تالا: اس ایپ سے نیٹ ورک کی تمام درخواستوں کو روکتا ہے۔ اجازت مکمل طور پر منسوخ کرنے کے لیے، سسٹم ایپ کی معلومات کی ترتیبات استعمال کریں۔ سسٹم ایپ کی معلومات کھولیں Android ترتیبات میں اجازتوں اور بیٹری کے استعمال کا نظم کریں - LeanType ترتیبات میں نیٹ ورک رسائی غیر فعال ہے + LeanTypeDual ترتیبات میں نیٹ ورک رسائی غیر فعال ہے AI فراہم کنندہ پروف ریڈنگ کے لیے AI سروس منتخب کریں Google Gemini diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 1ddc5c74e..b360425b5 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -103,8 +103,8 @@ Tugma chegaralari Avtomatik kunduz/tun rejimi Tashqi ko\'rinish tizim sozlamalariga binoan - LeanType Imlo tekshiruvi - LeanType Imlo Tekshiruvi Sozlamalari + LeanTypeDual Imlo tekshiruvi + LeanTypeDual Imlo Tekshiruvi Sozlamalari Svayp bilan o\'chirish Yangi so\'zlarni o\'rganishni o\'chirib qo\'yish Sonlar qatori @@ -121,7 +121,7 @@ %s (Akkhor) Kiritish Qo\'shimcha kalitlar - LeanType Sozlamalari + LeanTypeDual Sozlamalari Tuzatishlar Simvollarni ko‘rsatish Uzoq bosish ko‘rsatish diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 94038377a..6a655a319 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -108,11 +108,11 @@ Xem trên GitHub Cát Tự động chọn ngày/đêm - Kiểm tra lỗi chính tả + LeanTypeDual Kiểm tra lỗi chính tả Luôn hiển thị gợi ý ở đây - Cài đặt - Cài đặt trình kiểm tra lỗi chính tả + LeanTypeDual Cài đặt + LeanTypeDual Cài đặt trình kiểm tra lỗi chính tả Chọn từ Chế độ một tay Tự động ẩn thanh công cụ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9543780fa..2d7abbc89 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -110,8 +110,8 @@ 如果禁用,剪贴板键将粘贴剪贴板内容(如果有的话) 标点后面自动插入空格 输入新字词时自动在标点后插入空格 - 拼写检查 - 拼写检查设置 + LeanTypeDual 拼写检查 + LeanTypeDual 拼写检查设置 %s (Akkhor) 禁用新字词学习 更多键 @@ -120,7 +120,7 @@ 自动日间/夜间模式 外观将遵循系统设置 输入 - 设置 + LeanTypeDual 设置 额外键 长按空格键将提示输入法选择菜单 使用空格键更改输入法 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 86d3dfbee..d528639bf 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -94,8 +94,8 @@ "更多語言…" 實驗功能 %s分鐘 - 拼字檢查 - 拼字檢查設定 + LeanTypeDual 拼字檢查 + LeanTypeDual 拼字檢查設定 Emoji 強制無痕模式 剪貼板記錄 @@ -104,7 +104,7 @@ 啟用剪貼板記錄 在標點符號後自動插入空格 在輸入字串後自動插入空格 - 設定 + LeanTypeDual 設定 輸入 文字改正 "選擇輸入法" diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3601ea64f..8c350b649 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -107,7 +107,7 @@ 字典 紫羅蘭 外觀 - LeanType 設定 + LeanTypeDual 設定 建議 實驗功能 雜項 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index e82e053ca..dfd355053 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -68,7 +68,7 @@ %s (1452-2) - LeanType + LeanTypeDual Open-source keyboard with AI integration. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8454d9938..22f468d9a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,12 +6,12 @@ --> - LeanType Spell Checker - LeanType + LeanTypeDual Spell Checker + LeanTypeDual - LeanType Settings + LeanTypeDual Settings - LeanType Spell Checker Settings + LeanTypeDual Spell Checker Settings Vibrate on keypress @@ -631,7 +631,7 @@ Internal lock: Blocks all network requests from this app. To revoke permission entirely, use system App Info settings. Open System App Info Manage permissions and battery usage in Android settings - Network access is disabled in LeanType settings + Network access is disabled in LeanTypeDual settings AI Provider Select AI service for proofreading Google Gemini @@ -1644,4 +1644,10 @@ New dictionary: Custom Prompt for Key %1$d Tag / Label (e.g. \'French\', \'Grammar\', \'Rewrite\') Selected Keyword Functions: + Swipe up for shortcuts + Swipe up from the top row, including the number row when shown. Edit the menu in Secondary layouts. + Swipe down for shortcuts + Swipe down from the bottom letter row. Edit the menu in Secondary layouts. + Swipe-up shortcuts + Swipe-down shortcuts diff --git a/app/src/test/java/helium314/keyboard/Shadows.kt b/app/src/test/java/helium314/keyboard/Shadows.kt index 7c27dfe31..e75dbbe49 100644 --- a/app/src/test/java/helium314/keyboard/Shadows.kt +++ b/app/src/test/java/helium314/keyboard/Shadows.kt @@ -31,8 +31,8 @@ class ShadowInputMethodManager2 : ShadowInputMethodManager() { @Implementation override fun getInputMethodList() = listOf( if (BuildConfig.BUILD_TYPE == "debug" || BuildConfig.BUILD_TYPE == "debugNoMinify") - InputMethodInfo("helium314.keyboard.debug", "LatinIME", "LeanType debug", null) - else InputMethodInfo("helium314.keyboard", "LatinIME", "LeanType", null), + InputMethodInfo(BuildConfig.APPLICATION_ID, "helium314.keyboard.latin.LatinIME", "LeanTypeDual debug", null) + else InputMethodInfo(BuildConfig.APPLICATION_ID, "helium314.keyboard.latin.LatinIME", "LeanTypeDual", null), ) @Implementation fun getShortcutInputMethodsAndSubtypes() = emptyMap>() diff --git a/app/src/test/java/helium314/keyboard/SwipeShortcutMenuTest.kt b/app/src/test/java/helium314/keyboard/SwipeShortcutMenuTest.kt new file mode 100644 index 000000000..9ba9b562f --- /dev/null +++ b/app/src/test/java/helium314/keyboard/SwipeShortcutMenuTest.kt @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard + +import android.content.Context +import android.content.res.AssetManager +import android.app.KeyguardManager +import android.os.Looper +import android.view.ContextThemeWrapper +import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import helium314.keyboard.event.HapticEvent +import helium314.keyboard.event.Event +import helium314.keyboard.keyboard.Key +import helium314.keyboard.keyboard.Keyboard +import helium314.keyboard.keyboard.KeyboardActionListener +import helium314.keyboard.keyboard.KeyboardId +import helium314.keyboard.keyboard.KeyboardLayoutSet +import helium314.keyboard.keyboard.KeyboardTheme +import helium314.keyboard.keyboard.MainKeyboardView +import helium314.keyboard.keyboard.PointerTracker +import helium314.keyboard.keyboard.PopupKeysKeyboardView +import helium314.keyboard.keyboard.SwipeShortcutMenu +import helium314.keyboard.keyboard.SwipeShortcutMenu.Direction.DOWN +import helium314.keyboard.keyboard.SwipeShortcutMenu.Direction.UP +import helium314.keyboard.keyboard.internal.keyboard_parser.LayoutParser +import helium314.keyboard.keyboard.internal.KeyboardState +import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode +import helium314.keyboard.latin.LatinIME +import helium314.keyboard.latin.R +import helium314.keyboard.latin.RichInputMethodSubtype +import helium314.keyboard.latin.common.InputPointers +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.settings.DebugSettings +import helium314.keyboard.latin.utils.LayoutType +import helium314.keyboard.latin.utils.LayoutUtilsCustom +import helium314.keyboard.latin.utils.SubtypeUtilsAdditional +import helium314.keyboard.latin.utils.prefs +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.* +import org.robolectric.Robolectric +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import java.util.Locale +import java.util.concurrent.TimeUnit +import kotlin.math.abs +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(ParameterizedRobolectricTestRunner::class) +@Config(sdk = [33], shadows = [ShadowInputMethodManager2::class, ShadowProximityInfo::class]) +class SwipeShortcutMenuTest(private val glideEnabled: Boolean) { + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "glide={0}") + fun gestureModes() = listOf(arrayOf(false), arrayOf(true)) + } + + private lateinit var context: Context + private lateinit var keyboard: Keyboard + private lateinit var view: MainKeyboardView + private lateinit var listener: KeyboardActionListener + private var eventTime = 10_000L + private var downTime = 0L + private var useTouchEvent = false + + @Before + fun setUp() { + val ime = Robolectric.setupService(LatinIME::class.java) + val assets = mock(AssetManager::class.java) + // Android uses slash-separated asset paths, even when Robolectric runs on Windows. + `when`(assets.list(anyString())).thenAnswer { + ime.assets.list(it.getArgument(0).replace('\\', '/')) + } + `when`(assets.open(anyString())).thenAnswer { + ime.assets.open(it.getArgument(0).replace('\\', '/')) + } + context = object : ContextThemeWrapper(ime, KeyboardTheme.getKeyboardTheme(ime).mStyleId) { + override fun getAssets() = assets + } + context.prefs().edit() + .putBoolean(Settings.PREF_SWIPE_UP_MENU, true) + .putBoolean(Settings.PREF_SWIPE_DOWN_MENU, true) + .putBoolean(Settings.PREF_POPUP_ON, false) + .remove(Settings.PREF_LAYOUT_PREFIX + LayoutType.SWIPE_UP.name) + .remove(Settings.PREF_LAYOUT_PREFIX + LayoutType.SWIPE_DOWN.name) + .apply() + Settings.getInstance().loadSettings(context) + LayoutUtilsCustom.onLayoutFileChanged() + KeyboardLayoutSet.onKeyboardThemeChanged() + keyboard = buildKeyboard() + view = MainKeyboardView(context) + view.setKeyboard(keyboard) + view.measure(exact(700), exact(400)) + view.layout(0, 0, 700, 400) + listener = mock(KeyboardActionListener::class.java) + view.setKeyboardActionListener(listener) + configureGlide() + } + + @After + fun tearDown() { + if (::view.isInitialized) { + useTouchEvent = false + event(MotionEvent.ACTION_CANCEL, 0, 0) + view.cancelAllOngoingEvents() + } + LayoutParser.clearCache() + } + + @Test + fun `seven bottom shortcuts align with z through m`() { + val row = keyboard.swipeShortcutRows.getValue(DOWN) + assertEquals("zxcvbnm", row.keys.map { it.code.toChar() }.joinToString("")) + val menu = SwipeShortcutMenu.Builder(context, keyboard, row, DOWN).build() + assertEquals( + listOf(KeyCode.ARROW_LEFT, KeyCode.ARROW_RIGHT, KeyCode.WORD_LEFT, KeyCode.WORD_RIGHT, + KeyCode.CLIPBOARD, KeyCode.NUMPAD, KeyCode.EMOJI), + menu.sortedKeys.map { it.code } + ) + row.keys.zip(menu.sortedKeys).forEach { (source, shortcut) -> + assertTrue(abs(center(source) - row.left - center(shortcut)) <= 1) + } + } + + @Test + fun `top row follows visible number row`() { + assertEquals("qwertyuiop", keyboard.swipeShortcutRows.getValue(UP).keys.map { it.code.toChar() }.joinToString("")) + val numbered = buildKeyboard(numberRow = true) + assertEquals("1234567890", numbered.swipeShortcutRows.getValue(UP).keys.map { it.code.toChar() }.joinToString("")) + assertEquals("zxcvbnm", numbered.swipeShortcutRows.getValue(DOWN).keys.map { it.code.toChar() }.joinToString("")) + } + + @Test + fun `nonalphabet layouts do not intercept swipes`() { + assertTrue(buildKeyboard(element = KeyboardId.ELEMENT_SYMBOLS).swipeShortcutRows.isEmpty()) + assertTrue(buildKeyboard(element = KeyboardId.ELEMENT_NUMBER).swipeShortcutRows.isEmpty()) + } + + @Test + fun `menu endpoints remain aligned at different item counts`() { + val row = keyboard.swipeShortcutRows.getValue(DOWN) + for (count in listOf(2, 3, 7, 10, 20)) { + val centers = row.centers(count) + assertTrue(abs(center(row.keys.first()) - centers.first()) <= 1) + assertTrue(abs(center(row.keys.last()) - centers.last()) <= 1) + assertEquals(count, centers.distinct().size) + } + assertEquals(center(row.keys[3]), row.centers(1).single()) + } + + @Test + fun `split layout preserves physical source centers`() { + val split = buildKeyboard(split = true) + assertTrue(split.mId.mIsSplitLayout) + val row = split.swipeShortcutRows.getValue(DOWN) + assertEquals(row.keys.map(::center), row.centers(row.keys.size)) + } + + @Test + fun `custom secondary menu preserves order text and all rows`() { + val layout = """[[{"label":"copy"},{"label":"Hello","code":${KeyCode.MULTIPLE_CODE_POINTS}}], [{"label":"!"}]]""" + // Use the standard layout editor's file naming and storage path. + val name = LayoutUtilsCustom.getLayoutName("My menu", LayoutType.SWIPE_DOWN) + "json" + val file = LayoutUtilsCustom.getLayoutFile(name, LayoutType.SWIPE_DOWN, context) + file.writeText(layout) + assertEquals(layout, file.readText()) + LayoutUtilsCustom.onLayoutFileChanged() + Settings.writeDefaultLayoutName(name, LayoutType.SWIPE_DOWN, context.prefs()) + LayoutParser.clearCache() + val row = keyboard.swipeShortcutRows.getValue(DOWN) + val menu = SwipeShortcutMenu.Builder(context, keyboard, row, DOWN).build() + assertEquals(3, menu.sortedKeys.size) + assertEquals(KeyCode.CLIPBOARD_COPY, menu.sortedKeys.first().code) + assertEquals("Hello", menu.sortedKeys[1].outputText) + assertEquals('!'.code, menu.sortedKeys.last().code) + file.writeText("""[[{"label":"paste"}]]""") + LayoutUtilsCustom.onLayoutFileChanged() + KeyboardLayoutSet.onKeyboardThemeChanged() + val edited = SwipeShortcutMenu.Builder(context, keyboard, row, DOWN).build() + assertEquals(KeyCode.CLIPBOARD_PASTE, edited.sortedKeys.single().code) + file.delete() + LayoutUtilsCustom.onLayoutFileChanged() + Settings.writeDefaultLayoutName(null, LayoutType.SWIPE_DOWN, context.prefs()) + } + + @Test + fun `downward swipe sends shortcut but not source letter`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_UP, x, y + source.height) + assertFalse(view.isShowingPopupKeysPanel()) + verify(listener).onCodeInput(eq(KeyCode.ARROW_LEFT), anyInt(), anyInt(), eq(false)) + verify(listener, never()).onCodeInput(eq('z'.code), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `number row swipe opens upward menu and q does not`() { + keyboard = buildKeyboard(numberRow = true) + view.setKeyboard(keyboard) + swipe(keyboard.getKey('1'.code)!!, UP) + verify(listener).onCodeInput(eq(KeyCode.UNDO), anyInt(), anyInt(), eq(false)) + clearInvocations(listener) + val q = keyboard.getKey('q'.code)!! + event(MotionEvent.ACTION_DOWN, center(q), q.y + q.height / 2) + event(MotionEvent.ACTION_MOVE, center(q), q.y - q.height / 2) + assertFalse(view.isShowingPopupKeysPanel()) + } + + @Test + fun `swiping from each bottom letter chooses corresponding item`() { + val row = keyboard.swipeShortcutRows.getValue(DOWN) + val codes = SwipeShortcutMenu.Builder(context, keyboard, row, DOWN).build().sortedKeys.map { it.code } + row.keys.zip(codes).forEach { (source, code) -> + clearInvocations(listener) + swipe(source, DOWN) + verify(listener).onCodeInput(eq(code), anyInt(), anyInt(), eq(false)) + verify(listener, times(1)).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + } + + @Test + fun `dragging across menu selects last item and keeps menu fixed`() { + val row = keyboard.swipeShortcutRows.getValue(DOWN) + val source = row.keys.first() + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, center(source), y) + event(MotionEvent.ACTION_MOVE, center(source), y + source.height) + event(MotionEvent.ACTION_MOVE, center(row.keys.last()), y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_UP, center(row.keys.last()), y + source.height) + verify(listener).onCodeInput(eq(KeyCode.EMOJI), anyInt(), anyInt(), eq(false)) + } + + @Test + fun `returning to starting height cancels without typing`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_MOVE, x, y) + event(MotionEvent.ACTION_UP, x, y) + assertFalse(view.isShowingPopupKeysPanel()) + verify(listener, never()).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `action cancel cannot commit a popup through phantom up`() { + val source = keyboard.getKey('m'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_CANCEL, x, y + source.height) + assertFalse(view.isShowingPopupKeysPanel()) + verify(listener, never()).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `locked keyboard cannot expose editable shortcuts`() { + val keyguard = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + shadowOf(keyguard).setIsDeviceLocked(true) + val locked = buildKeyboard() + assertTrue(locked.mId.mDeviceLocked) + assertTrue(locked.swipeShortcutRows.isEmpty()) + shadowOf(keyguard).setIsDeviceLocked(false) + } + + @Test + fun `second finger cancels before the popup event filter`() { + val source = keyboard.getKey('z'.code)!! + val other = keyboard.getKey('q'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + multiEvent(MotionEvent.ACTION_POINTER_DOWN or (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + listOf(0 to (x to y + source.height), 1 to (center(other) to other.y + other.height / 2))) + assertFalse(view.isShowingPopupKeysPanel()) + multiEvent(MotionEvent.ACTION_POINTER_UP, + listOf(0 to (x to y + source.height), 1 to (center(other) to other.y + other.height / 2))) + multiEvent(MotionEvent.ACTION_UP, listOf(1 to (center(other) to other.y + other.height / 2))) + verify(listener, never()).onCodeInput(eq(KeyCode.ARROW_LEFT), anyInt(), anyInt(), anyBoolean()) + verify(listener, never()).onCodeInput(eq('z'.code), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `non distinct multitouch cannot select through its synthetic up`() { + context.prefs().edit().putBoolean(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, true).apply() + view = MainKeyboardView(context) + view.setKeyboard(keyboard) + view.measure(exact(700), exact(400)) + view.layout(0, 0, 700, 400) + view.setKeyboardActionListener(listener) + configureGlide() + useTouchEvent = true + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + multiEvent(MotionEvent.ACTION_POINTER_DOWN or (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + listOf(0 to (x to y + source.height), 1 to (300 to 100))) + assertFalse(view.isShowingPopupKeysPanel()) + verify(listener, never()).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `custom symbol toggle drives real keyboard state`() { + val name = LayoutUtilsCustom.getLayoutName("Mode menu", LayoutType.SWIPE_DOWN) + "json" + val file = LayoutUtilsCustom.getLayoutFile(name, LayoutType.SWIPE_DOWN, context) + file.writeText("""[[{"label":"symbol_alpha"}]]""") + LayoutUtilsCustom.onLayoutFileChanged() + Settings.writeDefaultLayoutName(name, LayoutType.SWIPE_DOWN, context.prefs()) + LayoutParser.clearCache() + val switches = mock(KeyboardState.SwitchActions::class.java) + val state = KeyboardState(switches) + state.onLoadKeyboard(0, null, false) + clearInvocations(switches) + val stateListener = object : KeyboardActionListener by KeyboardActionListener.EMPTY_LISTENER { + override fun onPressKey(primaryCode: Int, repeatCount: Int, isSinglePointer: Boolean, hapticEvent: HapticEvent) { + state.onPressKey(primaryCode, isSinglePointer, 0, null) + } + override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) { + state.onEvent(Event.createSoftwareKeypressEvent(primaryCode, 0, x, y, isKeyRepeat), 0, null) + } + override fun onReleaseKey(primaryCode: Int, withSliding: Boolean) { + state.onReleaseKey(primaryCode, withSliding, 0, null) + } + } + view.setKeyboardActionListener(stateListener) + try { + swipe(keyboard.getKey('z'.code)!!, DOWN) + verify(switches, times(1)).setSymbolsKeyboard() + verify(switches, never()).setAlphabetKeyboard() + } finally { + Settings.writeDefaultLayoutName(null, LayoutType.SWIPE_DOWN, context.prefs()) + file.delete() + LayoutUtilsCustom.onLayoutFileChanged() + } + } + + @Test + fun `fast swipe with no move event still commits once`() { + val source = keyboard.getKey('z'.code)!! + event(MotionEvent.ACTION_DOWN, center(source), source.y + source.height / 2) + event(MotionEvent.ACTION_UP, center(source), source.y + source.height * 2) + verify(listener).onCodeInput(eq(KeyCode.ARROW_LEFT), anyInt(), anyInt(), eq(false)) + verify(listener, times(1)).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `horizontal glide keeps its path when it later moves outward`() { + for (direction in listOf(UP, DOWN)) assertGlideSurvivesOutwardMove(direction) + } + + @Test + fun `holding shortcut menu cannot deliver a pending glide update`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + // Sampled by the gesture arbiter but still below the shortcut activation threshold. + event(MotionEvent.ACTION_MOVE, x, y + source.width / 4) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.SECONDS) + assertTrue(view.isShowingPopupKeysPanel()) + assertFalse(mockingDetails(listener).invocations.any { it.method.name == "onUpdateBatchInput" }, + "A pending glide update must not run after a shortcut menu takes ownership") + verify(listener, never()).onStartBatchInput() + event(MotionEvent.ACTION_UP, x, y + source.height) + verify(listener).onCodeInput(eq(KeyCode.ARROW_LEFT), anyInt(), anyInt(), eq(false)) + } + + @Test + fun `cancelled shortcut does not disable the next glide`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_MOVE, x, y) + event(MotionEvent.ACTION_UP, x, y) + assertFalse(view.isShowingPopupKeysPanel()) + verify(listener, never()).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + assertGlideSurvivesOutwardMove(UP) + } + + @Test + fun `coalesced sideways history prevents a late outward menu`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + eventTime += 100 + val motion = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, + (x + source.width).toFloat(), y.toFloat(), 0) + motion.addBatch(eventTime + 100, x.toFloat(), (y + source.height).toFloat(), 1f, 1f, 0) + assertEquals(1, motion.historySize) + view.processMotionEvent(motion) + motion.recycle() + assertFalse(view.isShowingPopupKeysPanel()) + } + + @Test + fun `changing keyboards cancels the menu without committing`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertTrue(view.isShowingPopupKeysPanel()) + view.setKeyboard(buildKeyboard(numberRow = true)) + assertFalse(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_UP, x, y + source.height) + verify(listener, never()).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + + @Test + fun `shortcut press and release are delivered only on commitment`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + verify(listener, never()).onPressKey(KeyCode.ARROW_LEFT, 0, true, HapticEvent.NO_HAPTICS) + event(MotionEvent.ACTION_UP, x, y + source.height) + val order = inOrder(listener) + order.verify(listener).onPressKey(KeyCode.ARROW_LEFT, 0, true, HapticEvent.NO_HAPTICS) + order.verify(listener).onCodeInput(eq(KeyCode.ARROW_LEFT), anyInt(), anyInt(), eq(false)) + order.verify(listener).onReleaseKey(KeyCode.ARROW_LEFT, false) + } + + @Test + fun `disabled menus and ordinary taps keep original behavior`() { + context.prefs().edit().putBoolean(Settings.PREF_SWIPE_DOWN_MENU, false).apply() + Settings.getInstance().loadSettings(context) + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_UP, x, y) + verify(listener).onCodeInput(eq('z'.code), anyInt(), anyInt(), eq(false)) + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertFalse(view.isShowingPopupKeysPanel()) + } + + @Test + fun `sideways start cannot later become an outward shortcut`() { + val source = keyboard.getKey('z'.code)!! + val x = center(source) + val y = source.y + source.height / 2 + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x + source.width, y) + event(MotionEvent.ACTION_MOVE, x, y + source.height) + assertFalse(view.isShowingPopupKeysPanel()) + } + + @Test + fun `period long press still opens normal punctuation popup`() { + val period = keyboard.getKey('.'.code)!! + event(MotionEvent.ACTION_DOWN, center(period), period.y + period.height / 2) + PointerTracker.getPointerTracker(0).onLongPressed() + assertTrue(view.isShowingPopupKeysPanel()) + } + + @Test + fun `popup placement does not depend on initiating key or view padding`() { + view.setPadding(9, 5, 7, 3) + for (direction in listOf(UP, DOWN)) { + val row = keyboard.swipeShortcutRows.getValue(direction) + for (source in listOf(row.keys.first(), row.keys.last())) { + val x = center(source) + view.paddingLeft + val y = source.y + source.height / 2 + view.paddingTop + event(MotionEvent.ACTION_DOWN, x, y) + val panel = assertNotNull(view.showSwipeShortcutMenu(PointerTracker.getPointerTracker(0), direction)) + val popup = panel as PopupKeysKeyboardView + val menu = assertNotNull(popup.keyboard) + menu.sortedKeys.zip(row.centers(menu.sortedKeys.size)).forEach { (key, expected) -> + val actual = popup.getContainerView().x + popup.x + popup.paddingLeft + center(key) + assertTrue(abs(actual - view.paddingLeft - expected) <= 1, "$direction $actual != $expected") + } + popup.dismissPopupKeysPanel() + event(MotionEvent.ACTION_CANCEL, x, y) + } + } + } + + private fun buildKeyboard( + numberRow: Boolean = false, split: Boolean = false, element: Int = KeyboardId.ELEMENT_ALPHABET + ): Keyboard = KeyboardLayoutSet.Builder(context, EditorInfo()) + .setKeyboardGeometry(700, 400) + .setSubtype(RichInputMethodSubtype.get( + SubtypeUtilsAdditional.createEmojiCapableAdditionalSubtype(Locale.ENGLISH, "qwerty", true) + )) + .setNumberRowEnabled(numberRow) + .setSplitLayoutEnabled(split) + .disableTouchPositionCorrectionData() + .build().getKeyboard(element) + + private fun swipe(key: Key, direction: SwipeShortcutMenu.Direction) { + val x = center(key) + val y = key.y + key.height / 2 + val endY = y + if (direction == UP) -key.height else key.height + event(MotionEvent.ACTION_DOWN, x, y) + event(MotionEvent.ACTION_MOVE, x, endY) + assertTrue(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_UP, x, endY) + } + + private fun configureGlide() { + view.setMainDictionaryAvailability(true) + PointerTracker.setClipboardInlineInputActive(false) + view.setGestureHandlingEnabledByUser(glideEnabled, false, false) + } + + private fun assertGlideSurvivesOutwardMove(direction: SwipeShortcutMenu.Direction) { + val row = keyboard.swipeShortcutRows.getValue(direction) + val source = row.keys.first() + val y = source.y + source.height / 2 + clearInvocations(listener) + event(MotionEvent.ACTION_DOWN, center(source), y) + for (key in row.keys.drop(1).take(3)) { + event(MotionEvent.ACTION_MOVE, center(key), y) + } + if (glideEnabled) verify(listener).onStartBatchInput() + else verify(listener, never()).onStartBatchInput() + val x = center(row.keys[3]) + val outwardY = y + if (direction == UP) -source.height else source.height + event(MotionEvent.ACTION_MOVE, x, outwardY) + assertFalse(view.isShowingPopupKeysPanel()) + event(MotionEvent.ACTION_UP, x, outwardY) + val completions = mockingDetails(listener).invocations.filter { it.method.name == "onEndBatchInput" } + assertEquals(if (glideEnabled) 1 else 0, completions.size) + if (glideEnabled) { + val points = completions.single().arguments.single() as InputPointers + assertEquals(5, points.pointerSize) + assertEquals(center(source), points.xCoordinates[0]) + assertEquals(x, points.xCoordinates[4]) + assertEquals(outwardY, points.yCoordinates[4]) + verify(listener, never()).onCodeInput(anyInt(), anyInt(), anyInt(), anyBoolean()) + } + } + + private fun event(action: Int, x: Int, y: Int) { + eventTime += 100 + if (action == MotionEvent.ACTION_DOWN) downTime = eventTime + val motion = MotionEvent.obtain(downTime, eventTime, action, x.toFloat(), y.toFloat(), 0) + if (useTouchEvent) view.onTouchEvent(motion) else view.processMotionEvent(motion) + motion.recycle() + } + + private fun multiEvent(action: Int, points: List>>) { + eventTime += 100 + val properties = points.map { (id, _) -> MotionEvent.PointerProperties().apply { + this.id = id + toolType = MotionEvent.TOOL_TYPE_FINGER + } }.toTypedArray() + val coordinates = points.map { (_, point) -> MotionEvent.PointerCoords().apply { + x = point.first.toFloat() + y = point.second.toFloat() + pressure = 1f + size = 1f + } }.toTypedArray() + val motion = MotionEvent.obtain(downTime, eventTime, action, points.size, properties, coordinates, + 0, 0, 1f, 1f, 0, 0, 0, 0) + if (useTouchEvent) view.onTouchEvent(motion) else view.processMotionEvent(motion) + motion.recycle() + } + + private fun center(key: Key) = key.x + key.width / 2 + private fun exact(size: Int) = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY) +} diff --git a/app/src/test/java/helium314/keyboard/latin/AppIdentityTest.kt b/app/src/test/java/helium314/keyboard/latin/AppIdentityTest.kt new file mode 100644 index 000000000..744e2d755 --- /dev/null +++ b/app/src/test/java/helium314/keyboard/latin/AppIdentityTest.kt @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard.latin + +import android.app.Application +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.view.inputmethod.InputMethodManager +import helium314.keyboard.ShadowInputMethodManager2 +import helium314.keyboard.latin.common.Links +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.util.Locale + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class, sdk = [33]) +class AppIdentityTest { + @Test + fun packageAndVersionMatchForkIdentity() { + val flavorSuffix = if (BuildConfig.FLAVOR == "offline") ".offline" else "" + val debugSuffix = if (BuildConfig.BUILD_TYPE in listOf("debug", "debugNoMinify")) ".debug" else "" + assertEquals("com.asafmah.leantypedual$flavorSuffix$debugSuffix", BuildConfig.APPLICATION_ID) + val (major, minor, patch) = BuildConfig.VERSION_NAME.split('.').map { it.toInt() } + assertEquals(4000 + major * 1000 + minor * 100 + patch * 10, BuildConfig.VERSION_CODE) + assertEquals(BuildConfig.APPLICATION_ID, RuntimeEnvironment.getApplication().packageName) + } + + @Test + @Config(shadows = [ShadowInputMethodManager2::class]) + fun testImeUsesTheActualApplicationId() { + val context = RuntimeEnvironment.getApplication() + val ime = context.getSystemService(InputMethodManager::class.java).inputMethodList.single() + assertEquals(BuildConfig.APPLICATION_ID, ime.packageName) + assertEquals("helium314.keyboard.latin.LatinIME", ime.serviceName) + } + + @Test + fun localizedAppAndServiceNamesRetainForkBranding() { + val context = RuntimeEnvironment.getApplication() + val names = listOf( + R.string.english_ime_name, R.string.ime_name, R.string.ime_settings, + R.string.spell_checker_service_name, R.string.android_spell_checker_settings + ) + for (locale in context.assets.locales) { + val config = Configuration(context.resources.configuration).apply { + setLocale(Locale.forLanguageTag(locale.replace('_', '-'))) + } + val localized = context.createConfigurationContext(config) + for (name in names) { + val label = localized.getString(name) + assertTrue("$locale: $label", label.contains("LeanTypeDual")) + } + } + val expectedName = if (BuildConfig.BUILD_TYPE in listOf("debug", "debugNoMinify")) + "LeanTypeDual debug" else "LeanTypeDual" + assertEquals(expectedName, context.applicationInfo.loadLabel(context.packageManager).toString()) + } + + @Test + fun componentsRetainNamespaceAndProvidersUseApplicationId() { + val context = RuntimeEnvironment.getApplication() + val info = context.packageManager.getPackageInfo( + context.packageName, PackageManager.GET_SERVICES or PackageManager.GET_PROVIDERS + ) + assertTrue(info.services.orEmpty().any { it.name == "helium314.keyboard.latin.LatinIME" }) + assertTrue(info.services.orEmpty().any { it.name == "helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService" }) + assertTrue(info.providers.orEmpty().any { it.authority == "${context.packageName}.fileprovider" }) + assertTrue(info.providers.orEmpty().any { it.authority == "${context.packageName}.androidx-startup" }) + } + + @Test + fun appUpdatesUseForkReleasesWithoutChangingVoicePluginReleases() { + assertEquals("https://api.github.com/repos/AsafMah/LeanType/releases/latest", Links.GITHUB_RELEASES_API) + assertEquals("https://github.com/AsafMah/LeanType/releases", Links.GITHUB_RELEASES_PAGE) + assertEquals( + "https://api.github.com/repos/LeanBitLab/LeanType-Voice-Plugin/releases/latest", + Links.VOICE_PLUGIN_RELEASES_API + ) + } +} diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 99f275c1e..2fca88332 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -1,6 +1,8 @@ package helium314.keyboard.settings import android.content.Context +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.LayoutType import androidx.test.core.app.ApplicationProvider import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -30,6 +32,20 @@ class SettingsContainerTest { assertTrue(res.isNotEmpty()) } + @Test + fun swipeMenusHaveOneSettingAndOneSecondaryLayoutEach() { + val keys = listOf( + Settings.PREF_SWIPE_UP_MENU, Settings.PREF_SWIPE_DOWN_MENU, + Settings.PREF_LAYOUT_PREFIX + LayoutType.SWIPE_UP.name, + Settings.PREF_LAYOUT_PREFIX + LayoutType.SWIPE_DOWN.name + ) + val settings = container.filter("") + keys.forEach { key -> + assertEquals(1, settings.count { it.key == key }) + assertTrue(container[key] != null) + } + } + @Test fun testFilterPerformance() { val searches = listOf("a", "b", "c", "theme", "color", "sound", "vib", "dictionary", "key", "layout") diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 000000000..86a9f6ef0 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,73 @@ +# Preparing LeanTypeDual releases + +Use `.github/workflows/release.yml` on the intended `v2`-based candidate. +Do not run `tools/release.py`: it is upstream maintenance tooling that changes +translations, branding and dictionaries, not this fork's release command. + +## Metadata and local checks + +Keep `app/build.gradle.kts` authoritative for `versionName` and `versionCode`. +The fork's code formula is `4000 + major * 1000 + minor * 100 + patch * 10`. +Prepare `docs/releasenote/release_notes_v.md`, starting with +`# LeanTypeDual `, and +`fastlane/metadata/android/en-US/changelogs/.txt`. +Move the fork's changelog entries into a dated release-preparation section and +leave a fresh Unreleased section. The preparation date is not a publication date. + +```text +python -B -m unittest discover -s docs/scripts -p "test_*.py" -v +python docs/scripts/generate_release_notes.py +``` + +The notes script uses Gradle's version for branch builds (including branch `v2`). +For tag builds it also requires the exact tag `v`. Missing, empty, +header-only or wrong-version notes fail rather than becoming placeholder notes. +Historical upstream notes are archival, not the fork's release source. + +## Signed candidate, before tagging + +The registered `Release` workflow accepts a manual dispatch from a pushed +candidate branch: + +```text +gh workflow run release.yml --repo AsafMah/LeanType --ref +``` + +Confirm the resulting run's `headSha` is the candidate commit. Manual dispatch +builds and uploads artifacts only; it cannot create a GitHub release. +The runner needs repository secrets `KEYSTORE_BASE64`, `KEYSTORE_PASSWORD`, +`KEY_ALIAS`, and `KEY_PASSWORD`. All four are required. They must describe the +existing LeanTypeDual release key, not a new key. Never copy them locally or put +keystore material in logs/artifacts. + +CI runs the real native, minified release builds for standard, standardfull and +offline. It requires exactly one APK per flavor, verifies signatures including +Android 5/6 v1 compatibility at each flavor's minimum SDK, and requires R8 +mappings. The `release-apks` artifact contains three APKs, SHA-256 checksums, +notes and the exact source commit; `release-verification` contains signature +reports and R8 mappings. + +Download artifacts into a new directory. Independently run `apksigner verify +--verbose --print-certs` and `aapt dump badging` on every candidate APK. Check: + +- Package, version name/code, label, min SDK, target SDK, native ABIs and + non-debuggable status agree with the source and release notes. +- The signer certificate SHA-256 matches the corresponding **published** old + APK for every retained flavor/package identity, not just one flavor. +- Standard and standardfull share the base package; offline has `.offline`. + There is no offlinelite output. Do not infer settings migration from matching + certificates. +- Exercise the minified APK on a device, including swipe activation/editing, + cancellation, ordinary typing and the old-version upgrade path. A signed + build or a successful debug smoke test does not establish this. + +## Approval boundary + +Open a focused preparation PR targeting **v2**, and verify its base after +creation; do not target old `main`. Record the candidate run, signer comparisons, +artifacts and remaining device checks in the PR. + +Merging that PR, pushing the final version tag and publishing a public release +each require explicit maintainer approval. A version-tag workflow creates only +a **draft**, after its signed-build gates pass. Review the draft's notes, all +three APKs and checksums before separately approving publication. diff --git a/docs/releasenote/release_notes_v2.0.0.md b/docs/releasenote/release_notes_v2.0.0.md new file mode 100644 index 000000000..725005f5a --- /dev/null +++ b/docs/releasenote/release_notes_v2.0.0.md @@ -0,0 +1,51 @@ +# LeanTypeDual 2.0.0 + +## A clean fork reset + +LeanTypeDual 2.0.0 starts from LeanBitLab/LeanType commit +`2a9d5fecb67c12b834ba2a1e80ffcdc8ab7d6720`, with the fork's branding and +opt-in swipe shortcut menus. This is a clean reset, **not feature parity with +LeanTypeDual 0.3.0**: the old fork's custom functionality has not been carried +forward. Upstream artwork and the plugin ecosystem retain their provenance. + +## Editable, row-aligned swipe shortcuts + +- Enable **Swipe up for shortcuts** and/or **Swipe down for shortcuts** in + **Settings > Gesture typing**. Both are off by default. +- Swipe up from the top row or down from the bottom letter row to open a menu + aligned with that row. When the number row is visible, it becomes the top + swipe-up row. +- Edit **Swipe-up shortcuts** and **Swipe-down shortcuts** in + **Settings > Secondary layouts**. +- Cancelling a menu does not commit a shortcut. Pending glide updates and + long-press/repeat timers are stopped when the menu takes over, preventing + stray input and allowing the next gesture to start cleanly. + +The app, keyboard, spell checker, settings and APKs are branded **LeanTypeDual**. +Update checks use this fork's releases rather than upstream LeanType releases. + +## Choose one of three APKs + +All APKs use version `2.0.0` / version code `6000` and include ARMv7 and ARM64 +native libraries (`armeabi-v7a`, `arm64-v8a`). + +| APK | Minimum Android | Package | Network and installation permissions | +| --- | --- | --- | --- | +| `1-LeanTypeDual_2.0.0-standard-release.apk` | 6.0 (SDK 23) | `com.asafmah.leantypedual` | Internet permission for online features; no package-install permission | +| `1-LeanTypeDual_2.0.0-standardfull-release.apk` | 6.0 (SDK 23) | `com.asafmah.leantypedual` | Same online implementation as standard, plus package-install permission for the in-app updater | +| `2-LeanTypeDual_2.0.0-offline-release.apk` | 5.0 (SDK 21) | `com.asafmah.leantypedual.offline` | No Internet permission; optional plugins are imported separately | + +Standard and standardfull are alternatives for the **same installed app**, not +side-by-side packages. Offline has its own package and can coexist with either. +There is **no offlinelite APK** in this release. Optional plugins can have higher +Android or architecture requirements than the keyboard itself. + +## Before upgrading from 0.3.0 + +The retained variants keep their LeanTypeDual package identities and use the +existing release signing key. The version code increases from `4300` to `6000`. +That preserves Android's package/signature upgrade path; it does **not** guarantee +that every old setting, layout, plugin configuration or custom feature migrates. +Export anything important using the old app's available backup/export functions +before upgrading, and review your settings afterwards. Offlinelite users do not +have a same-package 2.0.0 replacement. diff --git a/docs/scripts/generate_release_notes.py b/docs/scripts/generate_release_notes.py index 073bad456..a0ec2ccea 100755 --- a/docs/scripts/generate_release_notes.py +++ b/docs/scripts/generate_release_notes.py @@ -1,54 +1,52 @@ #!/usr/bin/env python3 import os +from pathlib import Path import re +import sys -def main(): - script_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.dirname(os.path.dirname(script_dir)) - - # 1. Try to get version from tag name (if running in GitHub Actions) - ref_name = os.environ.get('GITHUB_REF_NAME') - version_name = None - if ref_name and ref_name.startswith('v'): - version_name = ref_name[1:] - print(f"Detected version name from GITHUB_REF_NAME: {version_name}") - - # 2. Fall back to build.gradle.kts if not running in action or tag not matching - if not version_name: - gradle_path = os.path.join(project_root, 'app', 'build.gradle.kts') - if os.path.exists(gradle_path): - with open(gradle_path, 'r', encoding='utf-8') as f: - gradle_content = f.read() - version_name_match = re.search(r'versionName\s*=\s*"([^"]+)"', gradle_content) - if version_name_match: - version_name = version_name_match.group(1) - print(f"Parsed version name from build.gradle.kts: {version_name}") - - if not version_name: - print("Error: Could not determine version name") - return - - # 3. Locate the existing release notes file - releasenote_dir = os.path.join(project_root, 'docs', 'releasenote') - source_path = os.path.join(releasenote_dir, f'release_notes_v{version_name}.md') - temp_path = os.path.join(releasenote_dir, 'release_notes_temp.md') - - if not os.path.exists(source_path): - print(f"Error: Release note file {source_path} not found") - # Write a fallback file so the build/release step doesn't fail - with open(temp_path, 'w', encoding='utf-8') as f: - f.write(f"Release notes for version {version_name}") - return - # 4. Copy to release_notes_temp.md - with open(source_path, 'r', encoding='utf-8') as sf: - content = sf.read() - - with open(temp_path, 'w', encoding='utf-8') as df: - df.write(content) - - print(f"Successfully copied {source_path} to {temp_path}") - -if __name__ == '__main__': - # ponytail: copy pre-generated release notes file to temp.md - main() +def main(): + project_root = Path(__file__).resolve().parents[2] + notes_dir = project_root / "docs" / "releasenote" + output = notes_dir / "release_notes_temp.md" + try: + # Never leave a previous version's notes available after a failed run. + output.unlink(missing_ok=True) + gradle = (project_root / "app" / "build.gradle.kts").read_text(encoding="utf-8") + versions = re.findall(r'^\s*versionName\s*=\s*"([^"]+)"', gradle, re.MULTILINE) + if len(versions) != 1: + raise ValueError("Expected one versionName in app/build.gradle.kts") + version = versions[0] + if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", version): + raise ValueError(f"Invalid versionName: {version!r}") + + ref = os.environ.get("GITHUB_REF", "") + if os.environ.get("GITHUB_REF_TYPE") == "tag" or ref.startswith("refs/tags/"): + tag = os.environ.get("GITHUB_REF_NAME") or ref.removeprefix("refs/tags/") + if tag != f"v{version}": + raise ValueError(f"Tag {tag!r} does not match versionName {version!r}") + + source = notes_dir / f"release_notes_v{version}.md" + content = source.read_bytes() + lines = content.decode("utf-8").splitlines() + heading = f"# LeanTypeDual {version}" + if not lines or lines[0] != heading: + raise ValueError(f"{source.name} must start with {heading!r}") + body = re.sub(r"", "", "\n".join(lines[1:]), flags=re.DOTALL).strip() + if not body: + raise ValueError(f"{source.name} must include a body, not just a heading") + if re.fullmatch( + r"(?:TODO|TBD|Coming soon|Release notes for version \S+)[.!]?", + body, re.IGNORECASE, + ): + raise ValueError(f"{source.name} must not contain only placeholder notes") + output.write_bytes(content) + print(f"Validated and copied {source.name} to {output.name}") + return 0 + except (OSError, UnicodeError, ValueError) as error: + print(f"Error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/scripts/test_generate_release_notes.py b/docs/scripts/test_generate_release_notes.py new file mode 100644 index 000000000..00ff5ee0d --- /dev/null +++ b/docs/scripts/test_generate_release_notes.py @@ -0,0 +1,122 @@ +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +class ReleaseNotesTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + (self.root / "app").mkdir() + scripts = self.root / "docs" / "scripts" + scripts.mkdir(parents=True) + self.script = scripts / "generate_release_notes.py" + shutil.copyfile(Path(__file__).with_name(self.script.name), self.script) + self.gradle = self.root / "app" / "build.gradle.kts" + self.gradle.write_text('versionName = "2.0.0"\n', encoding="utf-8") + self.notes_dir = self.root / "docs" / "releasenote" + self.notes_dir.mkdir() + self.source = self.notes_dir / "release_notes_v2.0.0.md" + self.content = "# LeanTypeDual 2.0.0\n\nEditable swipe shortcuts.\n" + self.source.write_text(self.content, encoding="utf-8") + self.output = self.notes_dir / "release_notes_temp.md" + + def run_script(self, ref_type="", ref_name="", ref=""): + env = dict(os.environ, GITHUB_REF_TYPE=ref_type, + GITHUB_REF_NAME=ref_name, GITHUB_REF=ref) + return subprocess.run( + [sys.executable, str(self.script)], env=env, + capture_output=True, text=True, encoding="utf-8", + ) + + def assert_failed(self, result, message): + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn(message, result.stderr) + self.assertFalse(self.output.exists(), "Failed run left publishable notes") + + def test_local_build_uses_gradle_version(self): + result = self.run_script() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.output.read_bytes(), self.source.read_bytes()) + + def test_v2_branch_is_not_a_version_tag(self): + result = self.run_script("branch", "v2", "refs/heads/v2") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.output.read_bytes(), self.source.read_bytes()) + + def test_version_shaped_branch_still_uses_gradle(self): + result = self.run_script("branch", "v9.9.9", "refs/heads/v9.9.9") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.output.read_bytes(), self.source.read_bytes()) + + def test_matching_tag(self): + result = self.run_script("tag", "v2.0.0", "refs/tags/v2.0.0") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.output.read_bytes(), self.source.read_bytes()) + + def test_tag_ref_without_ref_type_still_checks_version(self): + self.assert_failed( + self.run_script(ref_name="v2.0.1", ref="refs/tags/v2.0.1"), + "does not match", + ) + + def test_mismatched_tag_rejects_stale_output(self): + self.output.write_text("Stale notes", encoding="utf-8") + self.assert_failed(self.run_script("tag", "v2.0.1"), "does not match") + + def test_incomplete_version_tag_rejected(self): + self.assert_failed(self.run_script("tag", "v2"), "does not match") + + def test_missing_notes(self): + self.source.unlink() + self.assert_failed(self.run_script(), "No such file") + + def test_empty_notes(self): + self.source.write_text(" \n", encoding="utf-8") + self.assert_failed(self.run_script(), "must start with") + + def test_placeholder_notes(self): + self.source.write_text("Release notes for version 2.0.0", encoding="utf-8") + self.assert_failed(self.run_script(), "must start with") + + def test_heading_only_notes(self): + self.source.write_text("# LeanTypeDual 2.0.0\n\n", encoding="utf-8") + self.assert_failed(self.run_script(), "must include a body") + + def test_placeholder_body_under_valid_heading(self): + for body in ("TODO", "TBD", "Coming soon.", "Release notes for version 2.0.0"): + with self.subTest(body=body): + self.source.write_text(f"# LeanTypeDual 2.0.0\n\n{body}\n", encoding="utf-8") + self.assert_failed(self.run_script(), "placeholder") + + def test_comment_only_body_under_valid_heading(self): + self.source.write_text( + "# LeanTypeDual 2.0.0\n\n\n", + encoding="utf-8", + ) + self.assert_failed(self.run_script(), "must include a body") + + def test_wrong_version_in_heading(self): + self.source.write_text("# LeanTypeDual 2.0.1\n\nChanges\n", encoding="utf-8") + self.assert_failed(self.run_script(), "must start with") + + def test_invalid_gradle_version(self): + self.gradle.write_text('versionName = "2"\n', encoding="utf-8") + self.assert_failed(self.run_script(), "Invalid versionName") + + def test_missing_gradle_version(self): + self.gradle.write_text("// No version\n", encoding="utf-8") + self.assert_failed(self.run_script(), "Expected one versionName") + + def test_tag_cannot_replace_missing_gradle(self): + self.gradle.unlink() + self.assert_failed(self.run_script("tag", "v2.0.0"), "No such file") + + +if __name__ == "__main__": + unittest.main() diff --git a/fastlane/metadata/android/en-US/changelogs/6000.txt b/fastlane/metadata/android/en-US/changelogs/6000.txt new file mode 100644 index 000000000..d8e3e2bcb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/6000.txt @@ -0,0 +1,7 @@ +LeanTypeDual 2.0.0 is a clean reset onto LeanBitLab/LeanType, not feature parity with 0.3.0. + +Opt-in, editable swipe shortcuts align with keyboard rows, follow the number row, and cancel safely. Fixed glide/long-press/repeat timers when menus take over. + +Enable in Gesture typing; edit in Secondary layouts. + +Three flavors: standard, standardfull, offline. No offlinelite. Retained package/signing identity; full settings migration is not guaranteed. Export important data before upgrading. diff --git a/layouts.md b/layouts.md index c7c6f8c51..5f8822c63 100644 --- a/layouts.md +++ b/layouts.md @@ -15,6 +15,43 @@ If you use an external glide typing library, you likely will have issues if your If the layout has exactly 2 keys in the bottom row, these keys will replace comma and period keys. More exactly: the first key will replace the first functional key with `"groupId": 1` in the bottom row, and the second key with replace the first key with `"groupId": 2`. +## Swipe shortcut menus + +Enable **Swipe up for shortcuts** and/or **Swipe down for shortcuts** in the gesture +settings. These work on the alphabet keyboard: up starts on its top character row +(the number row when visible); down starts on its bottom character row, above the +space/punctuation row. Space/delete gestures and ordinary long-press popups are +unchanged. Both menus are off by default. +Menus are unavailable while the device is locked, because editable entries can +contain private text or open clipboard history. + +Swipe outward, slide sideways to highlight an item, and release to activate it. +Returning to the starting height, sliding outside the menu horizontally, or adding +a second finger cancels the menu without typing the source key. + +Edit **Swipe-up shortcuts** or **Swipe-down shortcuts** in **Secondary layouts**, +using the same edit/import/select controls as other layouts. Both simple text and +JSON are supported. The menu reads every entry in file order into one horizontal +row; key widths are determined by the source row rather than the layout's `width` +properties. Labels, action codes, text output and selectors use the normal layout +parser. Nested long-press popups are not opened inside the menu. + +With seven items on QWERTY, the lower menu aligns with `z x c v b n m`. It stays in +the same place whichever key starts the swipe. With a different number of items, +the first and last stay at the row's first and last key centers and intermediate +items are distributed between them; a single item is centered. + +For example, a three-action menu: + +```json +[[{ "label": "undo" }, { "label": "copy" }, { "label": "paste" }]] +``` + +The built-in upper menu contains undo, redo, cut, copy, paste, select all and +clipboard. The lower menu contains left, right, word left, word right, clipboard, +numpad and emoji. Defaults live in `assets/layouts/swipe_up` and +`assets/layouts/swipe_down`. + ## Simple format * One key per line * Key format: [label] [popup keys], all separated by space, e.g. `a 0 + *` will create a key with text `a`, and the keys `0`, `+`, and `*` on long press