← Blog· Development log · einkbro-ios· 開發紀錄 · einkbro-ios
Zero to a full EinkBro App on iOS in three days
This week the EinkBro iOS port went from an empty repository to a
working WKWebView browser with near-complete feature parity with the Android
original — 57 commits and 53,470 lines between
Thursday morning and Saturday, built with Compose Multiplatform and Kotlin.
57
commits
+53,470
lines added (−1,781)
408
files in the tree
3
days, Jul 16–18
Lines added per day
The shape is the story: big-bang port, parity marathon, then surgical gap-closing.
Every settings screen wired to real behavior — pickers, sub-editors, privacy enforcement, and a 4,400-line batch round.
enum pickerstoolbar layout editorlocale pickerPDF paper sizeauto-hide toolbar
Sat
Final fixes & updates
A four-domain FEATURE_GAPS audit, then an overnight run closing the remaining gaps and polishing the details.
AI task runnernative chat tabslock-screen TTSdark-mode fixesx.com fixCJK mojibake fix
Thursday — the port, then a real browser by nightfall
12 commits · +35,193 / −559
The day opened with the initial port fa15096: the entire
EinkBro Compose UI — 258 files, ~24,000 lines — compiling and running on iOS via
Compose Multiplatform, first as a UI catalog of every ported screen and dialog.
Tiny source-compatible android.*/androidx.* shims let the
original Android files compile unchanged, keeping the package layout diffable
against the Android tree.
An eight-phase migration plan was adopted — and all eight phases shipped the same day:
Phase 1–2: real WKWebView browser shell; NSUserDefaults-backed preferences and a Room KMP database.
Phase 4: privacy & blocking — adblock via content rule lists, incognito, per-site configs, UA switching, clear-data.
Phase 5–6: text-selection highlights and link context menus; then the services layer — translation, read-aloud TTS, GPT summarize.
Phase 7–8: save-as-PDF and offline web archives; the einkbro:// scheme, .webarchive opening, iPad layout, app icon.
Plus a cleanup refactor moving all injected JavaScript out of Kotlin strings into .js asset files.
Friday — the feature-parity marathon, Phases A–N
22 commits · +14,817 / −929
With the browser working, Friday was a systematic sweep through fourteen parity
phases, each one mapped against specific Android files and line references:
Foundation (A–C): a central BrowserAction dispatcher mirroring Android's toolbar/menu handlers, a much deeper WKWebView engine delegate, and a sweep making core settings actually take effect.
Interaction (D–G): fullscreen, keep-awake, and pull-to-refresh; find on page; touch-area paging zones, the gesture FAB, and multitouch gestures; split screen with a second web pane, open-link-here, and scroll sync.
Power features (H–J): a real GM_* userscript runtime with Room storage and a script manager; pure-Kotlin EPUB export with chapters, images, and append-to-book; app-data backup, bookmark import, and Instapaper.
Services (K–M): AI chat-with-web with query persistence and GPT prompt editors; OpenAI and Edge neural TTS engines; translation via the Google widget, per-site auto-translate, image OCR, and dual captions.
Settings (N): enum pickers, sub-editors, privacy enforcement, and the toolbar layout editor.
The phases were followed by same-day audit rounds: favicons and a live tab bar,
URL-bar search suggestions, a hide/reorder menu editor, removal of the system
edit menu in favor of the custom selection menu, and a 4,400-line settings batch
(locale picker, PDF paper size, auto-hide toolbar, vertical toolbar, reader
settings, zoom reflow, external-link consent).
Saturday — audit-driven gap closing and polish
23 commits · +3,460 / −293
Saturday started with FEATURE_GAPS.md47ae073,
a four-domain audit of what still differed from Android — then an overnight
session closed most of it:
AI: the task runner went live — built-in tasks plus a free-form agent 4bd7046 — and chat-with-web and agent tasks moved into native chat tabs d0dd4db; fixes for agent page snapshots, a translation flash, and a task-menu crash.
Media & reading: background TTS audio with lock-screen and Control Center transport controls; dark mode in reader mode.
Engine fidelity: an offline error page with retry, pinch-to-zoom restored alongside a real load-progress bar, x.com no longer escaping to Safari (with a reload-loop guard), and CJK search queries no longer arriving as mojibake.
Privacy & per-site: analytics fast-block, the DNT header, FastToggle whitelist editors, and per-site desktop viewport width.
Data portability: Chrome/Netscape bookmark HTML import with nested folders, per-site domain configs in the backup ZIP, and highlights export.
Theming: the dark-mode primary color was black-on-black; that fix plus dark dialog headers, dialog borders, the root cause of input dialogs dismissing on focus, and edge-to-edge bottom content.
Under the hood
The app is a single shared Kotlin module consumed by a thin SwiftUI host.
All UI, ViewModels, the preference layer, the Room KMP database, and Ktor
networking live in commonMain; the platform seam is a set of
expect/actual interfaces — most importantly WebViewEngine, whose iOS
actual wraps WKWebView with user scripts and JS message handlers. TTS rides on
AVFoundation, adblock on WKContentRuleList, preferences on
NSUserDefaults.
SwiftUI host — iosApp
thin wrapper: entry point, einkbro:// scheme, external hand-offs
Two decisions did a lot of quiet work this week: keeping the original Android
package layout so every file diffs cleanly against its Android counterpart, and
treating the Android app as the behavioral reference — features were ported by
reading the original implementation first, not re-imagined. With no test suite,
the loop was a fast Kotlin compile check plus driving each feature in the iOS
simulator.