Skip to content
Merged
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
34 changes: 34 additions & 0 deletions .github/workflows/mobile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Mobile checks

on:
pull_request:
paths:
- 'Mobile/**'
- '.github/workflows/mobile.yml'
push:
branches: [main]
paths:
- 'Mobile/**'
- '.github/workflows/mobile.yml'

permissions:
contents: read

jobs:
mobile:
runs-on: ubuntu-latest
defaults:
run:
working-directory: Mobile
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
cache-dependency-path: Mobile/package-lock.json
- run: npm ci
- run: npm test
- run: npx tsc --noEmit
- run: npx expo lint --no-cache
- run: npx expo export --platform ios --output-dir dist
47 changes: 47 additions & 0 deletions Mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

example

# generated native folders
/ios
/android

# Generated by expo prebuild (Continuous Native Generation)
ios/
android/
1 change: 1 addition & 0 deletions Mobile/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }
7 changes: 7 additions & 0 deletions Mobile/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}
41 changes: 41 additions & 0 deletions Mobile/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
This is an Expo/React Native mobile application. Prioritize mobile-first patterns, performance, and cross-platform compatibility.

## Expo has changed — do not trust your training data

Expo ships breaking changes every SDK release. APIs you remember are likely renamed, moved, or removed. Before writing any code that touches an Expo, EAS, or React Native API:

1. Read the major version of the `expo` package in `package.json`.
2. Fetch the matching versioned docs: `https://docs.expo.dev/versions/v<major>.0.0/`
3. For anything else, fetch https://docs.expo.dev/llms.txt — an index of all Expo docs with corrections to common LLM misconceptions. Follow its links to the specific page you need; never answer from memory.

## Commands

Use `bunx` instead of `npx` if the project uses bun (`bun.lock` present).

```bash
npx expo install <package> # ALWAYS use instead of npm/yarn/pnpm/bun add — resolves SDK-compatible versions
npx expo start # start the dev server
npx expo lint # lint
npx tsc --noEmit # typecheck
npx expo-doctor # diagnose dependency and config issues
npx expo install --fix # fix incompatible package versions
```

Run lint and typecheck before declaring any task done.

## Navigation & Routing

- Use **Expo Router** for all navigation. Routes live in `src/app/` — every file there is a screen, `_layout.tsx` files define navigators. Keep non-route code (components, hooks, utils) outside `src/app/`.
- Import `Link`, `router`, and `useLocalSearchParams` from `expo-router`.
- Docs: https://docs.expo.dev/router/introduction.md

## Building with EAS

Use EAS to build, sign, and submit the app in the cloud (`eas build`, `eas submit`) and to ship over-the-air updates (`eas update`) — no local Xcode or Android Studio required. Run EAS CLI as `bunx eas-cli <command>` in Bun projects, or `npx eas-cli@latest <command>` otherwise; substitute that for bare `eas` in docs examples.
Docs: https://docs.expo.dev/eas/index.md

## Rules

- If `ios/` and `android/` directories do not exist, they are generated (Continuous Native Generation). Never create or edit them by hand — configure native behavior in `app.json` and config plugins.
- Expo Go only includes its bundled native modules. After adding a library with native code, the app needs a development build: `npx expo run:ios|android` locally, or `eas build --profile development`.
- Prefer recommended Expo modules over third-party libraries, and check your available skills before adding dependencies. Docs: https://docs.expo.dev/versions/latest/index.md
1 change: 1 addition & 0 deletions Mobile/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
101 changes: 101 additions & 0 deletions Mobile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Kodi Reader for iOS

A native-feeling mobile variant of [Kodi Reader](https://github.com/dev-olly/kodi-reader),
built with Expo SDK 57, Expo Router, and `@expo/ui` SwiftUI components. iOS 26
Liquid Glass is used for the reader chrome, the tab bar, and native sheets.

## What it does

- Imports EPUB and PDF files from the Files app, or adds two bundled Project
Gutenberg samples (Alice in Wonderland, Frankenstein).
- Paginated EPUB reading with the same `reader.js` and `reader.css` engine as
the Mac app, hosted in a `WKWebView`. Swipe or tap the edges to turn pages,
tap the centre to show or hide the chrome.
- Highlights in five colours plus underline, with notes. Positions and
highlights use the same simplified-CFI anchors as the Mac app, so they survive
font and theme changes.
- Typography and theme controls (New York, Georgia, Palatino, Charter, Helvetica,
San Francisco; size, line spacing, margins, justify, hyphenation).
- Notes tab across all books with Markdown export via the share sheet.
- Open any web page in the in-app browser and save it as a reader-mode book
(Readability extraction, images inlined, packaged as a one-chapter EPUB).
- PDFs open in the native WebKit PDF viewer (read-only, no highlights yet).

## Layout

```
src/app/ Expo Router routes
(tabs)/library|notes|settings native tab bar with a Stack per tab
reader/contents.tsx table of contents (form sheet)
reader/typography.tsx appearance controls (form sheet, @expo/ui Form)
web/index.tsx in-app browser with "Save as Book"
note/[bookId]/[annotationId].tsx note editor (form sheet)
src/reader/ ReaderOverlay (the reader), ReaderWebView bridge, runtime install, settings → CSS
src/reader/runtime/ reader.js + reader.css copied from the Mac app, plus bridge.js
src/lib/epub/ unzip (fflate), OPF/NCX/nav parsing, path helpers
src/lib/web/article.ts Readability result → EPUB (port of ArticleEPUBBuilder)
src/lib/store.ts library.json store (positions, annotations, settings)
src/components/ GlassSurface, HighlightPalette, BookRow, AppearanceForm
```

Data lives under the app's Documents directory in `Kodi/`:
`library.json`, `Books/`, `Extracted/`, `Covers/`, `runtime/`.

Book IDs hash the complete publication identifier, or the file contents when an
identifier is absent. Reader web views can access only their book's extraction
directory, including a local copy of the trusted reader runtime. A content
security policy disables publisher scripts, frames and network requests.

## How the reader stays fast

- The reader is a persistent layer above the navigation stack
(`src/reader/ReaderOverlay.tsx`), not a route. Its web views are created once
at launch, so WebKit's content process is already running when a book opens.
Opening a book is a page navigation, and reopening the same book is instant.
- Each chapter file is written with its own boot data (runtime options, reading
position, highlights) in `<head>`. `bridge.js` lays the page out on
`DOMContentLoaded` without waiting on round trips through the JS thread.
- A second web view preloads the next chapter behind the visible one. Reading
past a chapter end, or jumping to it from Contents, swaps it to the front.
- Set `DEBUG_TIMING` in `src/reader/ReaderWebView.tsx` to log load stages.

## Running

```sh
npm install
npx expo run:ios # needs Xcode 26 and CocoaPods
```

`@expo/ui` and `expo-glass-effect` need a development build; Expo Go is not
enough. On this Mac CocoaPods lives in a user-local Ruby 3.3, so run:

```sh
export PATH="$HOME/.local/ruby-env/bin:$PATH" LANG=en_US.UTF-8
npx expo run:ios --device "iPhone 17 Pro"
```

In development builds, `kodi://library?addSamples=1` imports the bundled
sample books without touching the UI (handy for simulator testing).

Liquid Glass renders on iOS 26 simulators and devices; older systems get a
translucent fallback with the same geometry.

## Not yet ported

Ask AI, cloud sync, Excalidraw drawings, and PDF highlights.

## Checks

Use Node.js 24, then run `npm ci`, `npm test`, `npx tsc --noEmit`,
`npx expo lint --no-cache`, and `npx expo export --platform ios`.
GitHub Actions runs these checks for mobile changes. The regression tests use
asynchronous native-service adapters; they do not replace an iOS simulator test.

On macOS, an optional native WebKit smoke test checks protected XHTML loading,
pagination, fragment navigation, position restore and highlight painting:

```sh
node tests/webkit-fixture.cjs /tmp/kodi-webkit-smoke
swiftc tests/webkit-smoke.swift -o /tmp/kodi-webkit-smoke-test
/tmp/kodi-webkit-smoke-test /tmp/kodi-webkit-smoke
```
73 changes: 73 additions & 0 deletions Mobile/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"expo": {
"name": "Kodi",
"slug": "kodi-mobile",
"version": "0.1.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "kodi",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"bundleIdentifier": "com.olly.KodiReader.mobile",
"supportsTablet": true,
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"CFBundleDocumentTypes": [
{
"CFBundleTypeName": "EPUB",
"LSHandlerRank": "Alternate",
"LSItemContentTypes": ["org.idpf.epub-container"]
},
{
"CFBundleTypeName": "PDF",
"LSHandlerRank": "Alternate",
"LSItemContentTypes": ["com.adobe.pdf"]
}
],
"LSSupportsOpeningDocumentsInPlace": false,
"UISupportsDocumentBrowser": false
}
},
"android": {
"package": "com.olly.kodireader",
"adaptiveIcon": {
"backgroundColor": "#245744",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
"expo-asset",
"expo-sharing",
[
"expo-splash-screen",
{
"backgroundColor": "#245744",
"image": "./assets/images/splash-icon.png",
"imageWidth": 96,
"dark": {
"backgroundColor": "#202522"
}
}
],
[
"expo-document-picker",
{
"iCloudContainerEnvironment": "Production"
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}
Binary file added Mobile/assets/images/android-icon-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/images/android-icon-foreground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/images/android-icon-monochrome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/images/kodi-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/images/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Mobile/assets/samples/alice.epub
Binary file not shown.
Binary file added Mobile/assets/samples/frankenstein.epub
Binary file not shown.
10 changes: 10 additions & 0 deletions Mobile/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require("eslint-config-expo/flat");

module.exports = defineConfig([
expoConfig,
{
ignores: ["dist/*"],
}
]);
9 changes: 9 additions & 0 deletions Mobile/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Learn more: https://docs.expo.dev/guides/customizing-metro/
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

// Sample books and the reader runtime ship as plain assets.
config.resolver.assetExts.push('epub', 'txt');

module.exports = config;
Loading
Loading