Manga reader content plugin: chapter directory, scroll and paged modes,
two-page spreads, fit and zoom, per-page comments and reading-position
restore. Chrome follows the host theme (light/dark, palette, fonts, icon
style) via the design system; the reading canvas is a media surface
(black by default, theme canvas optional). Supports page-folder resources
(subdirectories become chapters) and single-file comic archives —
CBZ/zip, CBR/rar, CB7/7z, CBT/tar, EPUB — whose pages are materialized on
first preview via the host's extractArchive.
Two equivalent ways to get a multi-chapter book into hoardodile:
- One archive: upload a single CBZ/CBR/CB7/CBT (chapter subdirectories inside the archive become chapters), or an EPUB with image pages. Non-zip formats list/extract through the bundled 7-Zip binary, so uploads of rar/7z/tar need that optional package installed.
- A whole book folder:
importLocalis non-recursive per direct item, but a directory item is packed recursively — put the whole book folder (with第1话/-style chapter subdirectories) into the import source, or browse to its parent in the folder importer and import it: it lands as ONE resource whose nested chapter paths the reader groups into chapters.
This plugin is developed entirely through the plugin toolchain — no hoardodile server, no web app:
pnpm build # bundle manifest + client + server hooks into dist/
pnpm dev # watch-build and serve the workbench (http://127.0.0.1:5199)
pnpm detect:smoke # run detect against testdata/pages through the real sandbox
pnpm test # vitestpnpm dev captures the server-side hook results (detect, sourceMeta,
searchMeta, listFiles, coverLocal) from the real worker sandbox and
feeds them to the workbench, so the iframe receives the same context the
app would push. It serves the flat page volume (testdata/pages via
--data); point it at testdata/cbz or testdata/book to work on the
archive / folder paths. Individual hooks can also be run or benchmarked
directly:
pnpm exec hoardodile plugin run listFiles testdata/pages --plugin-dir dist
pnpm exec hoardodile plugin run listFiles testdata/cbz --plugin-dir dist
pnpm exec hoardodile plugin bench listFiles testdata/pages --plugin-dir distArchive hooks materialize into a scratch cache under the OS temp dir
during plugin run/dev, mirroring the server's local/cache layout.
Committed fixture volumes under a single testdata/ root, regenerated by
pnpm testdata. Every volume is a flat directory — a resource holds
either a page folder or a single archive, never both:
testdata/pages/— a synthetic volume of ten generated pages,1.png…10.png. The filenames are deliberately unpadded so lexicographic and natural ordering disagree — the reader must show them in page order. Each page carries one marker bar per page number, so the current page is readable at a glance.testdata/cbz/— a two-chapter DEFLATE CBZ (第1话/and第2话/, five pages each, page numbers restarting per chapter) carrying the side-car junk real CBZs ship with (ComicInfo.xml,Thumbs.db) —listFilesmust skip those and keep only image pages.testdata/cbz-stored/— the same book re-packed with STORED entries, covering the uncompressed boundary of the container codec.testdata/cbz-nasty/— deep nesting, a ~90-char directory name and#/spaces/parens in page names.testdata/book/— the same book as a folder (chapter subdirectories), standing in for the folder-import path.
Real-world samples live in testdata-real/ (git-ignored; not
committed): small public-domain comics and EPUBs downloaded from the
Internet Archive, exercised during development.
pnpm testdata:real # fetch/verify samples, then run the hook suiteSizes are resolved dynamically from the archive.org metadata API, so a
source file that changed size is re-fetched instead of failing a stale
size check. On proxy-only networks the fetch needs Node's proxy support:
NODE_USE_ENV_PROXY=1 pnpm testdata:real (or set it in the shell).
The comic sample is deliberately a jp2-based archive.org scan
(Single Page Processed JP2 ZIP): it reproduces the real-world case
where the card cover renders but the reader pages are blank —
jp2/tiff/heic are not browser-decodable, archive pages are listed as
originals, and the host's transcode pipeline covers the first page
(cover) but the reader serves the raw extracted bytes. Archive cards
also show their page-count and width/height badges now that the
archive sourceMeta probes the first pages through the virtual path.
Verification drives the plugin hooks straight through
@hoardodile/host createPluginResourceAPI (the same path the
format integration tests use) rather than hoardodile plugin run:
plugin run/dev load the plugin into the sandbox without its
manifest, so the sandbox denies the container permission and every
archive hook is rejected — an upstream @hoardodile/cli limitation.
Known container limits: password-protected (encrypted) zip entries are rejected with a clear error, and ZIP64 archives (>4 GiB / >65535 entries) fall back to the yauzl parser without a dedicated fixture.
The fixture volumes hold resource content and nothing else: detect
rejects a page folder that mixes in non-image files, so a stray README
there would make the plugin stop matching its own fixture.
src/core/— framework-agnostic domain logic, fully unit-tested: source-shape classification (format.ts), chapter grouping and order (chapters.ts), the linear book model and navigation (book.ts), the versioned reading position and per-chapter progress (position.ts), and page-record building from container listings / extraction manifests (records.ts).src/main.ts— thin server hooks over the logic:detect,sourceMeta,searchMeta,coverLocal,listFiles,imageHashes. Archive pages are addressed as virtual paths (book.cbz!第1话/001.png) for probing/hashing/covers; full materialization happens only inlistFiles, i.e. on first preview (with the host's per-entry progress surfaced by the reader).src/render/— thin React adapters: the book hook (useMangaBook), chapter+page position (useMangaReaderState), first-materialization progress (useExtractProgress), the chapter directory drawer (ChapterList), and the scroll/paged views.
@hoardodile/* dependencies resolve from the npm registry (^0.1.9):
pnpm install pulls the published SDK directly — no tarballs, no
pnpm-workspace.yaml overrides.
- The plugin is MIT licensed (see
LICENSE). - Committed fixtures under
testdata/are synthetic (regenerated bypnpm testdata). Real-world samples fetched bypnpm testdata:realare public-domain works from the Internet Archive / Project Gutenberg and live only in the gitignoredtestdata-real/directory.
- hoardodile ≥ 0.1.9 — the built-in plugin marketplace
(Settings → Marketplace) and the batched asset-download API
(
download([…])) when the manifest declares"download": true. The marketplace's Readme and Release notes tabs and release readme assets are read by newer builds; older builds still list and install the plugin normally. "minAppVersion"inmanifest.jsondeclares the lowest hoardodile release this plugin runs on. Hosts below it refuse to install or update the plugin (the marketplace gates the install/update entries and zip uploads are blocked with an explanation), so bump it only when the plugin really needs a newer app. Omit it for plugins that support every release.- This plugin's version is independent of the hoardodile release version; bump it on user-visible changes.
- Dev loop: Node ≥ 24, pnpm 11.
Publish to the marketplace with one command:
# 1. Add the repository address to your registry repo's registry.json:
# { "version": 1, "plugins": ["https://github.com/<owner>/<repo>"] }
# 2. One-click release — release-it bumps the version in package.json AND
# manifest.json, writes CHANGELOG.md from Conventional Commits, commits,
# tags `v<version>` and pushes. The tag-triggered `.github/workflows/
# release.yml` then builds, packages (`release/<id>-<version>.zip` +
# `.sha256`) and publishes the GitHub release. No local `gh` CLI or token.
pnpm release <version>On main, with a clean working tree. The tag must match the manifest version
(v<manifest.version>) — the workflow fails otherwise. Then paste the registry
repo address once in Settings → Marketplace. The app reads the registry,
each plugin's manifest and its latest release — names, versions, permissions and
release notes come straight from GitHub, so the list never needs editing again.
The zip asset is <id>-<version>.zip (produced by hoardodile plugin package);
before the first release the plugin shows up with a "no release" state.
Prefer pushing a tag manually (no release-it)? It still works — the workflow does the same build/package/publish:
git tag v<version> && git push origin v<version>Local installs (zip upload in Settings → Plugins) still work for private packages.
The marketplace detail view shows a per-release Readme tab. Ship the
readme markdown in the readme/ folder as a bare README.md
fallback, plus one README.<locale>.md file per extra language (e.g.
readme/README.md, readme/README.zh.md) — release.yml uploads the whole
folder alongside the zip, so each release carries its own readme and
every version shows independent notes.
README.md is the fallback the app shows for any language without a
specific file, so English normally lives there and you do not need a
README.en.md. Use the app's supported language codes for the extra files
(zh, ja, de, es) — a region-coded name like README.zh-CN.md only
matches a UI language resolved to that exact code, so README.zh.md is what
Chinese users see.
pnpm readme:check (run by release.yml before publishing) gates this: it
fails the release if readme/ is absent-and-required, is not flat, ships no
README.md / README.<locale>.md, or references an image by a
nested/missing path.
The app resolves the readme for the user's UI language (exact locale → base
language → the README.md fallback); the release body always shows
in the Release notes tab.