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 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
- Panzoom: Pan/zoom functionality for manga pages
- 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
│ ├── panzoom/ # Custom pan/zoom implementation
│ ├── 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
The reader has complex text selection logic to prevent interference with panzoom drag:
beforeMouseDownhandler in MangaPage.svelte checks if click is on text- Text selection is only allowed within text boxes, not on background
- See
src/routes/[manga]/[volume]/+page.sveltefor implementation
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 Google Drive integration:
VITE_GDRIVE_CLIENT_ID=your_client_id
VITE_GDRIVE_API_KEY=your_api_key
These are only required for Google Drive sync. MEGA and WebDAV don't require 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
- 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 panzoom
- Migaku extension aggressively mutates DOM and can interfere with UI controls