Skip to content
Draft
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
68 changes: 0 additions & 68 deletions .github/workflows/build-release-apk.yml

This file was deleted.

151 changes: 151 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/leantype_banner_dark.svg">
Expand All @@ -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)

Expand All @@ -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.

---

Expand All @@ -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<br>`-standardfull-release.apk` | 🌿 Standard (FOSS)<br>`-standard-release.apk` | 🛡️ Offline<br>`-offline-release.apk` |
| :--- | :---: | :---: | :---: |
Expand All @@ -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** |

Expand Down Expand Up @@ -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!

Expand All @@ -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`).
Expand All @@ -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 |
| :--- | :--- | :--- |
Expand Down Expand Up @@ -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.
10 changes: 5 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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"
}
}

Expand Down
6 changes: 3 additions & 3 deletions app/src/debug/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
SPDX-License-Identifier: GPL-3.0-only
-->
<resources>
<string name="english_ime_name" translatable="false">LeanType debug</string>
<string name="spell_checker_service_name" translatable="false">LeanType debug Spell Checker</string>
<string name="ime_settings" translatable="false">LeanType debug Settings</string>
<string name="english_ime_name" translatable="false">LeanTypeDual debug</string>
<string name="spell_checker_service_name" translatable="false">LeanTypeDual debug Spell Checker</string>
<string name="ime_settings" translatable="false">LeanTypeDual debug Settings</string>

</resources>
6 changes: 3 additions & 3 deletions app/src/debugNoMinify/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
SPDX-License-Identifier: GPL-3.0-only
-->
<resources>
<string name="english_ime_name" translatable="false">LeanType debug</string>
<string name="spell_checker_service_name" translatable="false">LeanType debug Spell Checker</string>
<string name="ime_settings" translatable="false">LeanType debug Settings</string>
<string name="english_ime_name" translatable="false">LeanTypeDual debug</string>
<string name="spell_checker_service_name" translatable="false">LeanTypeDual debug Spell Checker</string>
<string name="ime_settings" translatable="false">LeanTypeDual debug Settings</string>
</resources>
11 changes: 11 additions & 0 deletions app/src/main/assets/layouts/swipe_down/swipe_down.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
[
{ "label": "left" },
{ "label": "right" },
{ "label": "word_left" },
{ "label": "word_right" },
{ "label": "clipboard" },
{ "label": "numpad" },
{ "label": "emoji" }
]
]
Loading
Loading