This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Mokuro Reader is a web-based manga reader for mokuro-processed manga. It's a SvelteKit 5 application with offline support, stat tracking, and Google Drive sync capabilities.
npm run dev- Start development servernpm run build- Build for productionnpm run preview- Preview production buildnpm test- Run tests with Vitestnpm run test:coverage- Run tests with coveragenpm run test:e2e- Run Playwright e2e tests (see Testing for port caveats)npm run check- Type-check with svelte-checknpm run check:watch- Type-check in watch modenpm run lint- Lint code (Prettier + ESLint)npm run format- Format code with Prettier
- Import/Upload: Users upload ZIP/CBZ files containing manga images and a
.mokuroJSON file - Storage: Data is stored in IndexedDB via Dexie:
volumestable: Metadata (title, UUID, page count, character count, thumbnail)volumes_datatable: Full page data and image files (File objects stored directly)
- Catalog: Browseable library of all imported volumes
- Reader: Renders manga pages with OCR text overlays and stat tracking
- Sync: Google Drive integration for syncing read progress and profiles across devices
- SvelteKit 5: Framework (uses new Svelte 5 runes:
$state,$derived,$effect) - Dexie: IndexedDB wrapper for storing volumes and files
- @zip.js/zip.js: ZIP file extraction
- Zoom architecture: Shared ZoomController + measurement-based correction drives zoom in all reader modes (
src/lib/reader/zoom-*.ts,paged-*.ts) - Flowbite Svelte: UI component library
- Tailwind CSS: Styling
- Vitest: Testing framework
src/
├── lib/
│ ├── anki-connect/ # Anki integration for vocabulary mining
│ ├── assets/ # Static assets (icons, etc.)
│ ├── catalog/ # Volume library management (Dexie DB, thumbnails)
│ ├── components/ # Svelte components
│ ├── consts/ # Application constants
│ ├── import/ # File import pipeline and processing
│ ├── reader/ # Core reader logic
│ ├── settings/ # Settings stores and profiles
│ ├── styles/ # Shared CSS styles
│ ├── types/ # TypeScript type definitions
│ ├── upload/ # Legacy upload utilities
│ ├── util/ # Utilities
│ │ └── sync/ # Multi-provider cloud sync
│ │ └── providers/
│ │ ├── google-drive/
│ │ ├── mega/
│ │ └── webdav/
│ ├── views/ # Top-level view components
│ └── workers/ # Web Workers for background tasks
├── routes/
│ ├── +page.svelte # Root page (hash router entry)
│ └── [...catchall]/ # SPA catchall for hash routing
└── app.d.ts # App-level type definitions
Routing: The app uses a hash-based router ($lib/util/hash-router.ts) with views loaded dynamically from $lib/views/. Routes like #/series/uuid or #/reader/uuid are handled client-side.
- Svelte Stores: Primary state management (writable, derived, readable stores)
- LocalStorage Sync: Many stores use
syncStoreutility to persist to localStorage - Key Stores:
volumes(settings/volume-data.ts): Read progress tracking per volumecurrentSettings(settings/settings.ts): Reader settings per volumeprofiles(settings/settings.ts): User profiles with different settingsmiscSettings(settings/misc.ts): Global app settings
Located in src/lib/util/sync/, the app supports multiple cloud storage providers:
| Provider | Auth Method | Status |
|---|---|---|
| Google Drive | OAuth2 implicit flow | Full support |
| MEGA | Email/password | Full support |
| WebDAV | URL + credentials | Full support |
Architecture:
- provider-interface.ts: Common
SyncProviderinterface all providers implement - provider-manager.ts: Manages provider instances and state
- unified-sync-service.ts: Provider-agnostic sync logic
- providers/: Provider-specific implementations
Google Drive specifics (providers/google-drive/):
- Uses OAuth2 implicit flow (access tokens only, ~1 hour expiry)
escapeNameForDriveQuery()must be used for file/folder names in API queries- Broad queries + client-side filtering is the correct pattern (Google scopes by app permissions)
$derivedand$derived.by()run for EVERY component instance- If a component appears N times, derived operations run N times
- Expensive operations or logging in derived causes severe performance issues
- Remove debug logging once the issue being debugged is resolved
The application uses Web Workers for parallel cloud downloads:
- worker-pool.ts: Manages multiple worker instances with memory limits
- download-worker.ts: Handles individual file downloads and ZIP extraction
- Memory management prevents overwhelming the browser during large batch downloads
- Configurable concurrency and throttling for low-memory devices
The application uses a V3 database (mokuro_v3) with Dexie. Data is split across three tables for performance:
| Table | Primary Key | Indexed Fields | Purpose |
|---|---|---|---|
volumes |
volume_uuid |
series_uuid, series_title |
Metadata, thumbnails |
volume_ocr |
volume_uuid |
— | OCR page data (text blocks) |
volume_files |
volume_uuid |
— | Image files (File objects) |
Key Types:
interface VolumeMetadata {
volume_uuid: string;
series_uuid: string;
series_title: string;
volume_title: string;
mokuro_version: string; // '' for image-only volumes
page_count: number;
character_count: number;
page_char_counts: number[]; // Cumulative per page
thumbnail?: File;
thumbnail_width?: number;
thumbnail_height?: number;
}
interface VolumeOCR {
volume_uuid: string;
pages: Page[];
}
interface VolumeFiles {
volume_uuid: string;
files: Record<string, File>;
}Usage:
import { db } from '$lib/catalog/db';
// Query volumes
const volumes = await db.volumes.toArray();
// Get full volume data
const metadata = await db.volumes.get(volume_uuid);
const ocr = await db.volume_ocr.get(volume_uuid);
const files = await db.volume_files.get(volume_uuid);Thumbnails are generated automatically on app load via startThumbnailProcessing().
Mokuro generates a .mokuro JSON file with this structure:
{
version: string,
title: string,
title_uuid: string,
volume: string,
volume_uuid: string,
pages: Page[], // Array of page data with OCR boxes
chars: number // Total character count
}Each Page contains blocks (text boxes) with bounding boxes, font size, and OCR text lines.
Three-tier settings system:
- Global defaults: Hardcoded in settings.ts
- Profile overrides: User-created profiles with custom settings
- Volume-specific overrides: Per-volume settings that override profile
Tracked per volume in the volumes store:
- Pages read
- Characters read (cumulative from mokuro data)
- Time spent reading (tracked by Timer component)
- Last read date and current page
All reader gesture handling (pan, pinch, tap, swipe, wheel, keyboard) goes
through the shared modules in src/lib/reader/input/ — see
docs/INPUT-CONTRACTS.md for the architecture and the contracts that
must not break. Highlights:
.textBoxis an input-routing protocol: double-tap there is the AnkiConnect capture gesture, mouse/pen drags are text selection (Yomitan/Migaku) — never pans, never zoom- Each surface owns its gestures via
PointerGestureTrackerconfig; Reader owns only keyboard + intent callbacks - Before starting any motion, handlers call their surface's
MotionGateintent method instead of ad-hocfinishNow()/stop()combinations
Always add relative z-10 to action button containers in modals.
Night mode applies a CSS filter to <dialog> elements (see app.html). The filter property creates a new stacking context, which resets all z-index relationships inside the dialog. Without explicit z-index, scrollable containers (overflow: auto/scroll) can capture click events instead of sibling button containers.
<!-- ✅ Correct - buttons will be clickable even with night mode filter -->
<div class="relative z-10 flex justify-end gap-2">
<Button>Cancel</Button>
<Button>Save</Button>
</div>
<!-- ❌ Wrong - buttons may not receive clicks when night mode is active -->
<div class="flex justify-end gap-2">
<Button>Cancel</Button>
<Button>Save</Button>
</div>Why this happens: Properties like filter, transform, opacity < 1, and will-change create new stacking contexts. Test modals with night mode ON to catch these issues.
Create a .env.local file for cloud provider integration:
VITE_GDRIVE_CLIENT_ID=your_client_id
VITE_GDRIVE_API_KEY=your_api_key
VITE_ONEDRIVE_CLIENT_ID=your_azure_app_client_id
VITE_GDRIVE_*: required only for Google Drive sync.VITE_ONEDRIVE_CLIENT_ID: required only for OneDrive sync. Register an Azure AD app (any Microsoft account tenant, "common" authority) and add the deploy origin as a Single-page application redirect URI. Scopes used:Files.ReadWrite,offline_access,User.Read. When unset, the OneDrive option is hidden from the cloud screen.- MEGA, WebDAV, and Local Folder require no env vars.
- Tests use Vitest with jsdom environment
- Component tests use @testing-library/svelte
- Run tests with
npm test - Example test files:
src/lib/util/count-chars.test.ts,src/lib/components/Settings/__tests__/QuickAccess.test.ts
npm run test:e2erunse2e/*.spec.ts. The config starts (or silently reuses) a dev server on port 5173.- Multi-worktree caveat: if another worktree's dev server already owns 5173, the suite would run against that worktree's code. Set
E2E_PORT=<free port>to start a dedicated server for the current worktree. E2E_CHROMIUM=/path/to/chromepoints Playwright at an existing browser binary instead of downloading one (e.g. a build under~/.cache/ms-playwright/).- The zoom specs import production modules (
zoom-controller.ts,zoom-layout.ts,page-detection.ts) through the Vite dev server and drive them against synthetic page strips.
- Add the setting to the
Settingstype insrc/lib/settings/settings.ts - Add default value to
defaultSettingsconstant - Update the settings UI component (e.g., ReaderToggles.svelte, ReaderSelects.svelte)
- Use the setting via the
currentSettingsderived store
The sync system (src/lib/util/sync/) uses a provider abstraction. To extend:
- For provider-specific features: modify the provider in
providers/<name>/ - For cross-provider features: update
unified-sync-service.ts - New providers must implement the
SyncProviderinterface fromprovider-interface.ts
Always use the Dexie instance from src/lib/catalog/db.ts:
import { db } from '$lib/catalog/db';
// Query volumes
const volumes = await db.volumes.toArray();
const volume = await db.volumes.get(volume_uuid);
// Get OCR and files separately (V3 split tables)
const ocr = await db.volume_ocr.get(volume_uuid);
const files = await db.volume_files.get(volume_uuid);
// Update volume metadata
await db.volumes.update(volume_uuid, { series_title: newTitle });This app is designed for Japanese learning extensions (Yomitan, Migaku, etc.) that manipulate text content in the DOM. These extensions can interfere with Svelte's reactivity.
Japanese learning extensions aggressively mutate the DOM:
- Yomitan: Wraps text in
<span>tags for dictionary lookups (relatively clean) - Migaku: Aggressively mutates text based on user settings (very invasive)
- Causes text carryover between manga pages
- Prevents UI elements from updating correctly
- Modifies settings panel controls
Use Svelte's {#key} blocks to force DOM recreation when extensions interfere. When a key changes, Svelte destroys the old DOM and creates a fresh one, bypassing extension mutations.
Why This Works for This App:
- Page changes are discrete user actions (not continuous scrolling)
- No form state to preserve during reading
- Performance cost acceptable for intentional page transitions
- Extensions can't carry stale state across fresh DOM nodes
Manga Page Layout (prevents text carryover):
{#key currentPage}
<MangaPage {pageData} />
{/key}Status Indicators (counters, timers, badges):
{#key tokenMinutesLeft}
<span>{tokenMinutesLeft}m</span>
{/key}Any Dynamic Text that extensions modify and needs to stay fresh.
Settings Panel: Migaku modifies the controls themselves, not just their parents. Keying the parent doesn't prevent this. Known issue with no current workaround.
Don't use keyed blocks for:
- Form inputs (will lose focus/state)
- Large component trees (performance impact)
- Static content (unnecessary)
- Content that SHOULD persist across updates
Test with Migaku enabled to catch DOM mutation issues.
CRITICAL: This repository uses git worktrees for ALL development work. The main working directory must remain on the main branch at all times.
Rules:
- The main directory (
/home/nathan/Projects/mokuro-reader) must ALWAYS stay onmainbranch - NEVER create feature branches or make commits directly in the main directory
- All changes must be made through git worktrees in
/home/nathan/Projects/mokuro-reader-worktrees/
Starting new work:
# Create a new worktree for a feature/fix
git worktree add ../mokuro-reader-worktrees/<branch-name> -b <branch-name>
# Or check out an existing remote branch
git worktree add ../mokuro-reader-worktrees/<branch-name> <branch-name>If asked to make changes without worktree context: Automatically create an appropriate worktree (e.g., fix/<issue> or feat/<feature>) and work there. Do not prompt—just create it and proceed.
Future note: The protected branch will eventually move from main to develop.
Don't auto-push during active development: If npm run dev or npm run preview is running, the user is actively iterating on changes. Only commit locally and wait for explicit instruction to push. This keeps the commit history clean and allows for squashing/amending before pushing.
Branch workflow: Development happens on develop. Merge into main for releases.
- Cloud provider auth tokens may expire (Google Drive ~1 hour, others vary)
- Large volume imports may cause memory pressure on low-end devices
- Text selection in reader requires special handling to not conflict with drag panning
- Migaku extension aggressively mutates DOM and can interfere with UI controls