Skip to content

feat(appkit): the app's logic in common code, for the mobile apps - #700

Draft
kdroidFilter wants to merge 9 commits into
feat/searchkitfrom
feat/mobile-ui
Draft

kdroidFilter wants to merge 9 commits into
feat/searchkitfrom
feat/mobile-ui

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

⚠️ Merge order

This PR is stacked on the search's port and must be merged after:

  1. feat(search): Kotlin Multiplatform search for iOS and Android 10+ SeforimLibrary#134 (the KMP search, NJA 0.4.0)
  2. feat(search): searchkit, the search without its UI for desktop, Android 10+ and iOS #698 (searchkit), this PR's base branch; once it's merged, this PR targets master.

NJA 0.4.0's Gradle plugin isn't on the Gradle Plugin Portal yet: here it was built from its v0.4.0 tag into mavenLocal, with mavenLocal() added to SeforimLibrary's pluginManagement (not committed).

Summary

First step of the mobile UI: everything that doesn't need the desktop goes to common code, so the Android and iOS apps only have their screens to write.

  • appkit: a new KMP module (JVM, Android 10+, iOS), taken from SeforimApp with its packages unchanged.
    • The reader: its state, use cases (content, commentaries, TOC, alt TOC, navigation, notes), paging, breadcrumbs, scrollbar and selection-bar logic.
    • The user's data: highlights, notes, favorites, history, category display, and the UserSettingsDb schema.
    • The settings: AppSettings and the general, fonts and display view models.
    • The onboarding's states, events and view models (region, user profile, type of installation).
    • The catalog (CatalogPresets, CatalogAccess, CatalogCache), DatabasePathProvider and the cities.
  • earthwidget and pagination: Android and iOS targets (Filament KMP renders there too). The solar position now uses kotlinx-datetime; the desktop views keep their java.util.Date overloads.
  • Off java.*:
    • atomicfu locks replace synchronized;
    • SeforimLibrary's :cache replaces the ConcurrentHashMap caches;
    • a small common LruCache replaces the access-order LinkedHashMaps;
    • kotlin.time / kotlin.uuid and FileKit paths replace the rest.
  • Reader panes: PaneSplitState replaces compose-splitpane's SplitPaneState, which is desktop only.
  • AccentColor: the enum is common; its Jewel and system colors stay on the desktop (AccentColors.kt).
  • Quote links: the addon moves to appkit's jvmMain (it's a JVM package); installedQuoteLinks is null on mobile.
  • BookContentViewModel and TabsViewModel, behind ports like searchkit's:
    • TabsHost: the window's tabs as the platform holds them; the desktop's DesktopTabsHost maps it onto its TabWorkspace.
    • TabsResolver (DesktopManager): to open tabs next to the reader's own.
    • SessionRestoreState (SessionManager): whether the session is being restored.
  • SessionManager: saving, restoring and migrating the session, naming its tabs. It now uses kotlinx-io files, in FileKit's databases directory as before. It reaches the shell through SessionShell: the desktops to save, the tab pictures to drop, and the end-to-end harness that turns it off. The desktop's implementation is DesktopSessionShell.
  • Dependency injection: AppKitBindings, a binding container contributed to AppScope, provides what every app's graph shares: settings, app state, catalog access, the user database and its stores, the books' repository, and the search's settings and history.
    • AppDatabases (expect/actual) opens the SQLite connections:
      • desktop: PersistentSqliteDriver for the books, JDBC with a busy timeout for the user database;
      • Android and iOS: searchkit's drivers for the books, their own for the user database.
    • The user tables are still created on each open (CREATE TABLE IF NOT EXISTS), now in common code.
    • The desktop's AppCoreBindings keeps only its own bindings: HTTP, Drive, search engine, delta updates, thumbnails, updater, DesktopManager and the shell's ports.
  • Small pieces:
    • CopyWithSource and BookErrorReport, on htmlparser's new htmlToText (jsoup/Ksoup Document.text()) and a common percent-encoding;
    • VisitEntry.destination() and openSiddurTab;
    • TabSearchSessionStore, formerly DesktopSearchSessionStore.
  • Compose resources: strings, fonts, pictures and files move to appkit.
    • Res keeps its package, now public; Android packs the resources.
    • The two OpenSans variable fonts lose the comma in their names, which Kotlin/Native refuses (they're unused in code).
    • FontCatalog, the window ViewModel store owner and the navigation helper move along.
  • Extraction of the database bundle: ExtractUseCase and BundleParts move to appkit, on Okio.
    • zstd-kmp decompresses: JNI on the desktop, native on Android and iOS. zstd-jni leaves SeforimApp.
    • Our own TarReader reads the archive: ustar, PAX headers for long paths and sizes over 8 GiB (as the packager writes them), GNU long names, base-256 sizes, header checksums.
    • Okio's HashingSource checks the bundle's sha256.
    • An entry climbing out of the databases directory is now refused.
    • GraalVM: the metadata gains zstd-kmp's library and the fields its JNI code sets.
  • DesktopManager stays on the desktop: it is the desktop shell (windows, Nucleus tab workspaces, geometry, drag & drop between windows). Mobile has neither windows nor virtual desktops, so it only implements the ports (TabsResolver, the desktops for SessionShell).
  • Tests: those of the moved code move with it, to commonTest (run on the JVM and the iOS simulator) or jvmTest.

Still on the desktop (next steps)

  • The Home widgets' layout and data.
  • The disk-space and download use cases (HttpFileDownloader, GitHubReleaseFetcher).
  • The DB delta updates.
  • The backup (UserDataBackup, PendingUserDataRestore, DataSettingsViewModel): it needs backup/backup-drive in KMP (zip, SHA, Drive's loopback OAuth).
  • The composables tied to Jewel (CatalogRow, TargumScrollbar, CategoryTreePanel…). AppGraph stays the desktop's root graph; a mobile graph only provides AppDatabases and its shell's ports.
  • The Jewel screens.
  • Then: pick the mobile design system and write the screens.

Verification

  • Compilation: :appkit for the JVM, the iOS simulator and Android; :earthwidget and :pagination likewise.
  • :appkit:jvmTest: 420 tests pass, including a bundle written as the packager does (zstd-jni, commons-compress) and extracted by ExtractUseCase,, including a new TabsViewModelTest on a fake host.
  • :appkit:iosSimulatorArm64Test: 374 tests pass (tar reader, extraction with zstd-kmp in native code).
  • GraalVM native image (macOS arm64): a new E2E scenario, ZAYIT_E2E_SCENARIO=extract with ZAYIT_E2E_BUNDLE, runs ExtractUseCase in the app as built.
    • The bundle: 157 MB in 3 parts, written as the packager does (POSIX tar, zstd -T2 --check, manifest with its sha256).
    • It extracts to a database with the expected sha256, both from the parts and from the single .tar.zst. Same result on the JVM.
    • Built with GraalVM Community, because Oracle's download timed out here; JNI is the same.
  • iOS simulator, real file system: IosExtractBundleTest (with -PseforimTestBundle and -PseforimTestBundleSha256) extracts the same bundle with zstd-kmp in native code.
    • From the 3 parts: the expected sha256, in 2.6 s.
    • From the single .tar.zst: the expected sha256 too.
    • Not tried on a physical device.
  • :earthwidget:jvmTest passes.
  • :SeforimApp:jvmTest: the same 31 failures as on master (WidgetDragUiTest, *ScreenshotTest), no new ones.
  • ktlint: ktlintCheck passes for appkit, earthwidget, pagination and SeforimApp.
  • Desktop app: it starts with the resources served by appkit, no MissingResourceException, and and renders the Home widgets with no exception. On quit, the common SessionManager saved the session it had restored at launch.

…le apps

A new KMP module, appkit (JVM, Android 10+, iOS), takes from SeforimApp what doesn't need the desktop: the reader's
state, use cases and paging, the user's data (highlights, notes, favorites, history, the UserSettingsDb schema), the
settings (AppSettings and the settings view models), the onboarding's states and view models, the catalog and the
cities. Packages are unchanged.

- earthwidget and pagination get Android and iOS targets (Filament KMP renders there too); the solar position runs
  on kotlinx-datetime, the desktop views keep their java.util.Date overloads.
- java.* replaced by common APIs: atomicfu locks, SeforimLibrary's :cache, LruCache, kotlin.time, FileKit paths.
- Reader pane splits: PaneSplitState instead of compose-splitpane's desktop-only SplitPaneState.
- AccentColor: the enum is common, its Jewel colors stay on the desktop (AccentColors.kt).
- The quote links addon moves to appkit's jvmMain (null on mobile); its DB test stays in SeforimApp.
- The tests of the moved code move with it: commonTest (run on the JVM and the iOS simulator) or jvmTest.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
- TabsViewModel drives a TabsHost port, the window's tabs as the platform holds them; the desktop's DesktopTabsHost
  maps it onto its TabWorkspace. A common test covers it on a fake host (JVM and iOS).
- BookContentViewModel reaches the desktop through two ports: TabsResolver (DesktopManager) to open tabs next to its
  own, SessionRestoreState (SessionManager) to know whether the session is being restored.
- pinnedTabLabel moves with it.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
SessionManager (saving, restoring and migrating the session, naming its tabs) moves to appkit, on kotlinx-io files
under FileKit's databases directory as before. It reaches the shell through a SessionShell port: the desktops to save
and the tabs' pictures to drop (the desktop's DesktopSessionShell, on DesktopManager and TabThumbnailStore), and the
end-to-end harness that turns it off.

DesktopManager stays on the desktop: it is the shell itself (windows, Nucleus tab workspaces, geometry, drag and drop
between windows), behind the ports it implements (TabsResolver, SessionShell's desktops).

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
…platform

- AppKitBindings (appkit, contributed to AppScope): the settings, the app state, the catalog access, the user
  database and its stores, the books' repository and the search's settings and history, for every app's graph.
- AppDatabases (expect/actual) opens the SQLite connections: on the desktop, PersistentSqliteDriver for the books and
  JDBC with a busy timeout for the user database; on Android and iOS, searchkit's drivers for the books and their own
  for the user database. The user tables are created on each open in common code, as before.
- ShnayimMikraStore moves to appkit; the desktop's AppCoreBindings keeps its own (HTTP, Drive, search engine, delta
  updates, thumbnails, updater, DesktopManager and the shell's ports) and provides AppDatabases.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
…e search's session store in common code

- CopyWithSource and BookErrorReport move to appkit, on htmlparser's new htmlToText (jsoup's Document.text() on the
  JVM and Android, Ksoup's on iOS) and a common percent-encoding of the mailto.
- VisitEntry.destination() and openSiddurTab: kotlin.uuid.
- DesktopSearchSessionStore, which only reads the common session, becomes appkit's TabSearchSessionStore.
- Their tests move with them.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
…es in common code

- The strings, fonts, pictures and files (composeResources) move to appkit, whose Res keeps its package
  (seforimapp.seforimapp.generated.resources), public for the apps' screens; Android packs them. The two OpenSans
  variable fonts lose the comma in their names, which Kotlin/Native refuses in an accessor (unused in code).
- FontCatalog (on isMacOsDesktop), the window ViewModel store owner and the navigation helper move to appkit, with
  FontCatalog's tests.
- CLAUDE.md describes appkit; the docs and font scripts point at the resources' new place.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
…d-kmp and our own tar reader

- ExtractUseCase and BundleParts move to appkit on Okio: zstd-kmp decompresses (JNI on the desktop, native on Android
  and iOS), a common TarReader reads the archive (ustar, POSIX extended headers for long paths and big sizes as the
  packager writes them, GNU long names, base-256 sizes, header checksums) and Okio's HashingSource checks the bundle's
  sha256. An entry climbing out of the databases directory is refused.
- zstd-jni leaves SeforimApp; its GraalVM metadata gains zstd-kmp's library and the fields its JNI code sets.
- Tests: the tar reader and the extraction in common code (JVM and iOS simulator), BundleParts on a fake file system,
  and a bundle written as the packager does (zstd-jni, commons-compress) extracted on the JVM.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
…ive image included)

ZAYIT_E2E_SCENARIO=extract with ZAYIT_E2E_BUNDLE naming a .tar.zst or its .part01 runs the onboarding's
ExtractUseCase into the run's output, with throwaway settings, and notes the extracted database's size and sha256.
On the GraalVM native image (macOS arm64), a 157 MB bundle in 3 parts written as the packager does (POSIX tar, zstd
-T2 with checksum, manifest with its sha256) extracts to the expected database, and so does the single .tar.zst.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
… file system

IosExtractBundleTest runs ExtractUseCase on FileSystem.SYSTEM in the simulator, with -PseforimTestBundle (a .tar.zst
or its .part01) and -PseforimTestBundleSha256 (the extracted database's); skipped without them. The 157 MB bundle in
3 parts written as the packager does extracts to the expected database in 2.6 s, and so does the single .tar.zst.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant