Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/update-badges.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Update README Badges

on:
schedule:
- cron: '0 0 * * *' # Midnight UTC
workflow_dispatch:

permissions:
contents: write

jobs:
update-badges:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Fetch GitHub stats
id: stats
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
REPO="LeanBitLab/HeliboardL"

# Latest version
VERSION=$(gh api repos/$REPO/releases/latest --jq '.tag_name' | sed 's/^v//' 2>/dev/null || echo "N/A")
echo "version=$VERSION" >> $GITHUB_OUTPUT

# Total downloads
DOWNLOADS=$(gh api repos/$REPO/releases --jq '[.[].assets[]?.download_count] | add // 0' 2>/dev/null || echo "0")
echo "downloads=$DOWNLOADS" >> $GITHUB_OUTPUT

# Stars
STARS=$(gh api repos/$REPO --jq '.stargazers_count' 2>/dev/null || echo "0")
echo "stars=$STARS" >> $GITHUB_OUTPUT

- name: Generate badge SVGs
env:
VERSION: ${{ steps.stats.outputs.version }}
DOWNLOADS: ${{ steps.stats.outputs.downloads }}
STARS: ${{ steps.stats.outputs.stars }}
run: |
mkdir -p docs/badges

# Format numbers with commas
DOWNLOADS_FMT=$(printf "%'d" "$DOWNLOADS" 2>/dev/null || echo "$DOWNLOADS")
STARS_FMT=$(printf "%'d" "$STARS" 2>/dev/null || echo "$STARS")

# Download version badge
cat > docs/badges/download.svg << EOF
<svg xmlns="http://www.w3.org/2000/svg" width="94" height="28" viewBox="0 0 94 28"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="94" height="28" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="50" height="28" fill="#555"/><rect x="50" width="44" height="28" fill="#7C4DFF"/><rect width="94" height="28" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="25" y="19" fill="#010101" fill-opacity=".3">Version</text><text x="25" y="18">Version</text><text x="72" y="19" fill="#010101" fill-opacity=".3">v${VERSION}</text><text x="72" y="18">v${VERSION}</text></g></svg>
EOF

# Downloads count badge
cat > docs/badges/downloads.svg << EOF
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="28" viewBox="0 0 105 28"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="105" height="28" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="64" height="28" fill="#555"/><rect x="64" width="41" height="28" fill="#7C4DFF"/><rect width="105" height="28" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="32" y="19" fill="#010101" fill-opacity=".3">Downloads</text><text x="32" y="18">Downloads</text><text x="84.5" y="19" fill="#010101" fill-opacity=".3">${DOWNLOADS_FMT}</text><text x="84.5" y="18">${DOWNLOADS_FMT}</text></g></svg>
EOF

# Stars badge
cat > docs/badges/stars.svg << EOF
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="28" viewBox="0 0 72 28"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="72" height="28" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="38" height="28" fill="#555"/><rect x="38" width="34" height="28" fill="#7C4DFF"/><rect width="72" height="28" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="19" y="19" fill="#010101" fill-opacity=".3">Stars</text><text x="19" y="18">Stars</text><text x="55" y="19" fill="#010101" fill-opacity=".3">${STARS_FMT}</text><text x="55" y="18">${STARS_FMT}</text></g></svg>
EOF

echo "Generated: v$VERSION | $DOWNLOADS_FMT downloads | $STARS_FMT stars"

- name: Update README badge URLs
run: |
# Replace shields.io URLs with local badge paths
sed -i 's|https://img.shields.io/github/v/release/LeanBitLab/HeliboardL?label=Download\&style=for-the-badge\&color=7C4DFF|docs/badges/download.svg|g' README.md
sed -i 's|https://img.shields.io/github/downloads/LeanBitLab/HeliboardL/total?style=for-the-badge\&color=7C4DFF\&label=Downloads|docs/badges/downloads.svg|g' README.md
sed -i 's|https://img.shields.io/github/stars/LeanBitLab/HeliboardL?style=for-the-badge\&color=7C4DFF|docs/badges/stars.svg|g' README.md

- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docs/badges/ README.md
git diff --staged --quiet || git commit -m "chore: update README badges [skip ci]"
git push
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,8 @@ docs/superpowers/
.agents
.kilo/
.antigravitycli/

.env

# AI agent config (personal, not shared)
.pi/
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

## [3.10.0] - 2026-06-20

### Added
- **Handwriting input** (Standard builds) — write characters on a recognition canvas using a
downloadable plugin, with a dedicated bottom-row layout and a toolbar key.
- **Auto-read OTP from SMS** — a one-time code from an incoming SMS is offered in the suggestion
strip while the keyboard is open; tap to insert. Uses a runtime, opt-in SMS permission.
- **Regex shortcuts in Text Expander** — expansion triggers can be matched by regular expression.

### Changed
- **Offline AI backend switched from ONNX Runtime to llama.cpp (GGUF).** The Offline build now
loads compact quantized **GGUF** models on-device with configurable sampling
(temperature / top-p / top-k / min-p); it now requires Android 8 (API 26).
- **Touchpad gestures reworked** into a fuller one-/two-finger suite (word select, word-by-word
navigation, space, copy/paste, cut/select-all, undo/redo, hold-to-backspace). Single-finger
double-tap now **selects the word** (previously deleted the selection).
- Release builds now target the **arm64-v8a** ABI only.

### Upstream
- Merged **LeanBitLab/LeanType v3.8.6** (from v3.8.3) — the source of the handwriting,
llama.cpp/GGUF, touchpad-gesture, and SMS-OTP changes above. Fork identity (LeanTypeDual, distinct
`applicationId`, two-thumb typing, the Gemini standard-AI layer, and the privacy tiers) is
preserved.

## [3.9.1] - 2026-06-11

### Fixed
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
### On top of that — LeanType's AI layer and quality-of-life features

- **[🤖 Multi-Provider AI](docs/FEATURES.md#supported-ai-providers)** - Proofread using **Gemini**, **Groq** (Llama 3, Mixtral), or **OpenAI-compatible** providers, with dynamic fetching of the latest models.
- **[🛡️ Offline AI](docs/FEATURES.md#5-offline-proofreading-privacy-focused)** - Private, on-device proofreading and translation using ONNX models (Offline build only).
- **[🛡️ Offline AI (GGUF)](docs/FEATURES.md#5-offline-proofreading-privacy-focused)** - Private, on-device proofreading and translation using local **GGUF models** powered by `llama.cpp` (Offline build only).
- **🌐 AI Translation** - Translate selected text using your chosen provider, with a separate model selector.
- **[✍️ Handwriting Input](docs/FEATURES.md#8-handwriting-input)** - Draw characters directly on a handwriting recognition canvas (Standard version, requires [Leantype-Handwriting-Plugin](https://github.com/LeanBitLab/Leantype-Handwriting-Plugin)).
- **[🧠 Custom AI Keys](docs/FEATURES.md#4-custom-ai-keys--keywords)** - Assign custom prompts, personas (#editor, #proofread), and labels/tags (themed capsules) to 10 customizable toolbar keys.
- **📝 Text Expander** - Shortcut → expansion with dynamic placeholders (`%clipboard%`, `%day%`, `%time12%`, `%cursor%`, lists), backspace-to-revert, and a guide.
- **📝 Text Expander** - Shortcut → expansion with dynamic placeholders (`%clipboard%`, `%day%`, `%time12%`, `%cursor%`, lists), regex shortcuts, backspace-to-revert, and a guide.
- **🧠 Smarter learned words** - *graduated trust* keeps a just-learned word below real-dictionary suggestions until you've used it a few times (no premature autocorrect to half-typed words); flag unknown words to **Add** or **Block** them via a Blocklist screen.
- **↩️ Undo word** - a toolbar key that reverts the last committed word back to its suggestion alternatives.
- **🗂️ Per-dictionary control** - enable or disable individual built-in and custom dictionaries.
Expand All @@ -38,7 +39,7 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
- **📸 Screenshot Suggestion & Clipboard** - Recently-taken screenshots are offered in the suggestion strip and saved to clipboard history.
- **🔎 Emoji Search** - Search emojis by name. *Requires loading an Emoji Dictionary.*
- **⚙️ Enhanced Customization** - Force auto-capitalization, fine-grained haptics, distinct incognito icon, reorganized settings, and more.
- **🔒 Privacy Choices** - Choose **Standard** (opt-in AI), **Offline** (network hard-disabled, offline model), or **Offline Lite** (no AI, ~20 MB).
- **🔒 Privacy Choices** - Choose **Standard** (opt-in AI, handwriting), **Offline** (network hard-disabled, offline GGUF model), or **Offline Lite** (no AI, ~20 MB).



Expand Down Expand Up @@ -73,20 +74,22 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
</tr>
</table>

> **⚠️ Note:** F-Droid releases might be delayed or stuck again due to reproducibility verification issues. For the latest version, use GitHub Releases or Obtainium.

### 📦 Choose Your Version

#### 1. Standard Version (`-standard-release.apk`)
* **Features:** Full suite including **AI Proofreading**, **AI Translation**, and **Gesture Library Downloader**.
* **Permissions:** Request `INTERNET` permission (used *only* when you explicitly use AI features).
* **Setup:** Use the built-in downloader for Gesture Typing. Configure AI keys in Settings.
* **Features:** Full suite including **AI Proofreading**, **AI Translation**, **Handwriting Input**, and **Gesture Library Downloader**.
* **Permissions:** Request `INTERNET` permission (used *only* when you explicitly use AI features, download plugins, or update libraries).
* **Setup:** Use the built-in downloader for Gesture Typing and Handwriting Input. Configure AI keys in Settings.

#### 2. Offline Version (`-offline-release.apk`)
* **Features:** All UI/UX enhancements and **Offline Neural Proofreading** (ONNX).
* **Features:** All UI/UX enhancements and **Offline Neural Proofreading** (via `llama.cpp` using local **GGUF models**).
* **Permissions:** **NO INTERNET PERMISSION**. Guaranteed at OS level.
* **Best For:** Privacy purists.
* **Manual Setup Required:**
* **Gesture Typing:** [Download library manually](https://github.com/erkserkserks/openboard/tree/46fdf2b550035ca69299ce312fa158e7ade36967/app/src/main/jniLibs) and load via *Settings > Gesture typing*.
* **Offline AI:** Download ONNX models and load via *Settings > AI Integration*. 👉 **[See Offline Setup Instructions](docs/FEATURES.md#3-offline-proofreading-privacy-focused)**
* **Offline AI:** Download GGUF models and load via *Settings > Advanced > GGUF Model (.gguf)*. 👉 **[See Offline Setup Instructions](docs/FEATURES.md#5-offline-proofreading-privacy-focused)**

#### 3. Offline Lite Version (`-offlinelite-release.apk`)
* **Features:** All UI/UX enhancements but **NO AI FEATURES**.
Expand Down
30 changes: 21 additions & 9 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,22 @@ if (keystorePropertiesFile.exists()) {
}

android {
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.asafmah.leantypedual"
minSdk = 21
targetSdk = 35
versionCode = 3910
versionName = "3.9.1"
versionCode = 4000
versionName = "3.10.0"

proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")

ndk {
abiFilters.addAll(arrayOf("armeabi-v7a", "arm64-v8a"))
abiFilters.addAll(arrayOf("arm64-v8a"))
}
}

// ONNX Runtime is used instead of llama.cpp native build

flavorDimensions += "privacy"
productFlavors {
create("standard") {
Expand All @@ -45,6 +43,7 @@ android {
create("offline") {
dimension = "privacy"
applicationIdSuffix = ".offline"
minSdk = 26
}
create("offlinelite") {
dimension = "privacy"
Expand Down Expand Up @@ -141,7 +140,7 @@ android {
path = File("src/main/jni/Android.mk")
}
}
// ndkVersion = "28.0.13004108"
ndkVersion = "28.0.13004108"

packaging {
jniLibs {
Expand Down Expand Up @@ -235,8 +234,21 @@ dependencies {
"standardOptimisedImplementation"("androidx.security:security-crypto:1.1.0-alpha06")

// local llm proofreading (offline)
// ONNX Runtime for T5 encoder-decoder grammar models
"offlineImplementation"("com.microsoft.onnxruntime:onnxruntime-android:1.17.3")
"offlineImplementation"("io.github.ljcamargo:llamacpp-kotlin:0.4.0")

// Force 16 KB page-aligned version of graphics-path
implementation("androidx.graphics:graphics-path:1.1.0")

// WorkManager — required by ML Kit Digital Ink plugin (loaded via DexClassLoader).
// ML Kit internally calls WorkManager.getInstance(context) using the host app context,
// so the host app must have WorkManagerInitializer registered in its manifest.
implementation("androidx.work:work-runtime-ktx:2.10.1")

// ML Kit Digital Ink Recognition — required by the handwriting plugin.
// ML Kit's internal asset manager and native library loader use the host app context,
// so the host app must compile and include the client library resources/libraries.
"standardImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")
"standardOptimisedImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")

// test
testImplementation(kotlin("test"))
Expand Down
3 changes: 0 additions & 3 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@
# Keep java-llama.cpp classes
-keep class de.kherud.llama.** { *; }

# ONNX Runtime configurations
-dontwarn com.google.protobuf.**
-keep class ai.onnxruntime.** { *; }

# Fix correct service name
-keep class helium314.keyboard.latin.utils.ProofreadService { *; }
Expand Down
34 changes: 33 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">


<uses-sdk tools:overrideLibrary="androidx.graphics.path,com.google.mlkit.vision.digitalink.recognition,com.google.mlkit.digitalink.common" />

<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Optional: only requested at runtime when the user enables the "Auto-read OTP" feature. -->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Expand All @@ -27,6 +29,8 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
android:allowBackup="false"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:remove="android:appComponentFactory"
tools:targetApi="p">

Expand Down Expand Up @@ -122,6 +126,34 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
android:resource="@xml/provider_paths" />
</provider>

<!-- Disable WorkManager auto-initializer; App implements Configuration.Provider instead.
Required for ML Kit Digital Ink plugin loaded via DexClassLoader. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>

<!-- Service for ML Kit component discovery.
Since the ML Kit libraries are loaded dynamically from the plugin APK,
we must register these in the host app manifest so ML Kit can find them. -->
<service
android:name="com.google.mlkit.common.internal.MlKitComponentDiscoveryService"
android:directBootAware="true"
android:exported="false">
<meta-data
android:name="com.google.firebase.components:com.google.mlkit.common.internal.CommonComponentRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
<meta-data
android:name="com.google.firebase.components:com.google.mlkit.vision.digitalink.internal.DigitalInkRecognitionRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>

</application>

<queries>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
[
{ "label": "alpha", "width": 0.15 },
{ "label": "clear_handwriting", "width": 0.15 },
{ "label": "space", "width": -1 },
{ "label": "delete", "width": 0.15 }
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
[
{ "label": "alpha", "width": 0.15 },
{ "label": "clear_handwriting", "width": 0.15 },
{ "label": "space", "width": -1 },
{ "label": "delete", "width": 0.15 },
{ "label": "action", "width": 0.15 }
]
]
6 changes: 3 additions & 3 deletions app/src/main/assets/locale_key_texts/ar.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
ه ﻫ|ه‍
ج چ
ش ڜ
ي ئ ى
ي ى ئ
ب پ
ل ﻻ|لا ﻷ|لأ ﻹ|لإ ﻵ|لآ
ا !fixedOrder!5 آ ء أ إ ٱ
ا !fixedOrder!5 أ ٱ إ آ ء
ك گ ک
ى ئ
ز ژ
و ؤ
punctuation !fixedOrder!7 ٕ|ٕ ٔ|ٔ ْ|ْ ٍ ٌ|ٌ ً ّ|ّ ٖ|ٖ ٰ ٓ|ٓ ِ|ِ ُ|ُ َ|َ ـــ|ـ
punctuation !fixedOrder!7 ّ◌|ّ ْ◌|ْ َ◌|َ ِ◌|ِ ُ◌|ُ ٍ◌ً◌ٌ◌|ٌ ٓ◌|ٓ ٰ◌ٕ◌|ٕ ٔ◌|ٔ ٖ◌|ٖ ـــ|ـ
« „ “ ”
» ‚ ‘ ’ ‹ ›

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/assets/locale_key_texts/fa.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[popup_keys]
ه ﻫ|ه‍ هٔ ة
ی ئ ي ﯨ|ى
ا !fixedOrder!5 ٱ ء آ أ إ
ا !fixedOrder!5 آ ء ٱ أ إ
ت ة
ک ك
و ؤ
Expand Down
Loading