Reader - ZornTaov/ZornCoMix GitHub Wiki

Reader

A web reader for the library, made to feel like a page-turning app: it runs in its own container, reads the library, and remembers where you are in every comic across devices. The only things it ever writes into the library are what you say a comic is, from its info page, and the covers you choose.

What it does differently

  • A comic is one stream of pages, however it is kept: a single archive that grows, one archive per chapter read in c001, c002… order, or a folder of loose pages. Chapter labels (from each archive's ComicInfo.xml, or its file name) are the table of contents.

  • Your place is the page, not its number. Progress is kept by what the page is, so it stays put when a comic is renumbered, a skipped page is put in before it, or it is cut into chapters. A comic moved with the web page's Move to another folder keeps your place, its settings and its cover. One moved by hand starts afresh, because nothing records where it came from.

  • New pages, not unread issues. The library has shelves for New pages (comics you had caught up on that have gained pages since; opening one starts at the first new page - and, for a series kept as one archive per hundred pages or per issue, the next archive once it arrives after you finished the last), Continue reading, Not started, Caught up and Ended. A comic is only "ended" when you mark it so in the scraper.

  • Random, beside Refresh, picks something to read for you, from wherever you are: the whole library at the top, or only the folder, series or author you're in. Each folder there has the same chance first, then a comic or series inside it, so a folder of forty one-shots doesn't crowd out everything else. A series opens at its first part you haven't read through, counted from its start (a series whose latest chapter you read before its first still starts at the first); one read part way opens where you left it. Nothing read through is picked. A random pick is only on trial: your place in it is kept once you've seen five of its pages or reached its last one, so one you open and put down never shows under Continue reading.

  • The ⋯ on a cover reads a comic from the start or marks it read or unread. Inside a folder or series it can also mark it read up to here, everything before it on that shelf included, for catching up on a comic started somewhere else. On a folder, series or author it marks everything in it at once.

  • Each comic has an info page, from About this comic on its ⋯ or Info in the reader's menu: what its ComicInfo.xml says (series, writer, artist, year, genre, tags, summary), its addresses on the web (the comic's first page, where updates carry on from, a chapter's first page) to open or copy, where it is kept in the library, and how its last update went. Edit changes its title, series, writer, artist, year, genre, tags, web address and summary:

    • for a comic the scraper keeps, into the info block of its mirror_metadata.json, and its archives' ComicInfo.xml at once. A box left empty keeps what the scraper works out, shown greyed;
    • for an archive from elsewhere, into its own ComicInfo.xml. A box left empty is taken out, and everything the page doesn't show (language, manga, and so on) is left as it was.

    Nothing is written while the library is being updated, or within five minutes of the scraper last touching that comic, and a change made over one the page never saw is refused.

  • Covers are yours to choose. Cover… in the reader's menu makes the page on screen the cover of the comic, or of the folder, series or author it's in. Edit… on any ⋯ menu (or Edit cover… on the info page) opens the cover dialog: upload a picture, fetch one from its web address, use a page of the comic by number, rotate it a quarter turn at a time (a wide logo stands up to fill a card), choose how it fits the card (Fill, cut to fit; Whole width; Whole height - a banner shows whole, with space above and below), or reset it. The preview is the card's own shape. Fit is only how the reader shows the cover, and can be set for a cover you never chose too, such as a comic's own first page. A rotated cover is made from the picture as it came, which the reader keeps, so turning it back gives the original exactly. A comic's cover is also saved beside its archive as <archive name>.png (or .jpg, .gif, .webp: the picture's own kind, byte for byte, so a see-through PNG stays one), and a folder's as cover.png and so on in it, where other readers look for them; a comic kept only as loose pages, a series and an author keep theirs in the reader alone, since a picture in a folder of pages would be packed as a page. A picture already on the shelf under those names is used when nothing is chosen, and a reset takes the reader's own picture off the shelf again unless it has been changed since.

  • Folders are the library's own. A folder holding only one comic, named after it (CBZs/MyComic/MyComic.cbz), is shown as that comic. Once anything else is put in that folder, beside it or in a folder below, it is a folder again, with the comic inside it alongside the rest. The bar under a folder, series or author is how far through everything in it you are, counted in pages, so a short extra read through moves it less than a long comic does.

  • It keeps up while the scraper works. A comic is looked at as you open it, and an archive caught in the middle of an append is read as it was a moment before.

  • Stand-in pages open the real thing: the video plays, a link opens, a Flash file downloads.

Reading

Turn Tap the left or right side, swipe, arrow keys, Page Up/Down, Space, or a Bluetooth page-turner
Menu Tap the middle (page slider, chapters, view options)
Jump Around the slider: first and last page, ten pages back or on, the previous or next chapter. Type a page number into the Page box to go straight to it
Jump keys Home and End for the first and last page, Shift with an arrow for ten pages, [ and ] for the previous and next chapter
Zoom Pinch, or double-tap the middle; drag to pan
Views One page, two pages (spreads and stand-ins shown alone, each chapter starting fresh), or scrolling (webtoon)
Fit Screen, width, height, or actual size. A page taller than the screen is read down before it turns
Direction Left to right or right to left; taps, swipes and arrows follow it
More Background colour, tap-zone width, swapping the sides, how many pages to have ready

A chapter jump stays inside the comic where its chapters are in one archive, and goes on to the next or previous archive where each chapter (or each numbered part of a series) is its own. Back goes to the start of the chapter you are in first, and only from there to the one before.

Settings are kept on the server, so a phone and a tablet read alike: for every comic, for one comic, or - where each chapter is its own archive, as with a comic another program scraped - for this series, every chapter of it at once. A chapter's own settings win over its series', and the series' over every comic's; a change made for the series takes the same setting off the chapter you are in, so it shows straight away. Pages around the one shown are fetched and decoded ahead, and a page once seen is cached for good, so a turn is instant. Volume keys cannot turn pages in a browser; a page-turner or keyboard can.

Running it

The image, ghcr.io/zorntaov/zcm-reader (formerly comic-reader, still published for now - see moving from comics-updater, which this moves the same way), is published from the repository the way the updater's is (see Docker for the tags and updating), so running it needs only reader/docker-compose.yml and docker compose up -d beside it. To build your own instead, from the repo root (the web page is built inside it, so only Docker is needed), and put its tag in the compose file's image::

docker build -f reader/Dockerfile -t zcm-reader:local .

The compose file takes its paths from the environment:

Variable
LIBRARY The library root (holding CBZs/ and Uncompressed/), mounted read-write so the info page can save; add :ro to the mount to keep it read-only
READER_DATA_DIR An empty folder for the reader's progress, settings and covers
READER_PASSWORD Optional; asked for by the browser, any user name
READER_PORT Default 8082
READER_SCAN_MINUTES How often the library is looked over; default 15. A comic found missing from where the last scan saw it (moved or renamed) brings the next scan forward to a few seconds later
READER_USER Default 1001:100, the library's owner
READER_LOG_LEVEL Default warning. info adds the server's own lines, every request among them; for chasing a fault

The container's log (docker compose logs -f) says how the reader started - port, library, data folder, whether there is a password - and what the first scan found, then a line whenever a scan finds a different number of comics. A first scan that finds none says so, with what to check: usually the library mounted at the wrong path, or a READER_USER that cannot read it.

Installing on a phone or computer

The reader is an installable web app (PWA), not an App Store or Play Store download. One web manifest serves Android and desktop browsers; Apple home-screen icons and metadata support iPhone and iPad. It opens full screen where the platform supports it, with its own icon and no browser address bar. The library and reading progress are still kept by your reader server, not copied into a separate app.

Use a reader build containing the install assets: :nightly, or any release after PR #1, which added them.

Open the reader over HTTPS before installing it. Put a TLS reverse proxy in front of the reader's port, using a certificate the device trusts. A plain http:// address on a LAN is enough for reading in a browser, but does not qualify for normal PWA installation. http://localhost and loopback addresses are exceptions for local development on that same device, not a way to install from another computer's LAN address. Keep the reader at the root of its own origin, not under a path such as /reader/.

Android: install and open full screen

  1. Open the reader's HTTPS address in Chrome or Edge in a normal browser tab and sign in if prompted. Do not use a private/incognito tab or another app's embedded browser.
  2. Open the browser's ⋮ menu and choose Install app or Add to home screen. If a submenu offers Install and Create shortcut, choose Install, then confirm. The wording varies by browser and Android version; a plain shortcut that opens a browser tab is not the installed app experience.
  3. Leave the browser and launch ZornCoMix Reader / ZCM Reader from the new home-screen or app-drawer icon. The installed reader requests fullscreen mode and opens without the browser's address and tab bars. Opening the address in a normal browser tab again does not use this installed mode.

If a previous attempt left a shortcut that still opens a browser tab, remove that shortcut and install again. Android and the browser control the remaining system UI, so a gesture/navigation indicator may still be visible. The reader's Full screen menu button is an additional option where the browser supports it; it is not a substitute for installing and launching the app.

iPhone and iPad: add to Home Screen and open as an app

  1. Open the reader's HTTPS address in Safari and sign in if prompted.
  2. Tap Share, then Add to Home Screen. If that action is missing, use Edit Actions to add it to the Share menu.
  3. Leave Open as Web App enabled if Safari shows that option, then tap Add.
  4. Leave Safari and launch ZCM Reader from its new Home Screen icon. Opening a Safari tab or a bookmark instead will still show Safari's browser controls.

The Home Screen app opens in iOS's standalone mode without Safari's address and tab bars. iOS may keep its status bar and home indicator: it does not promise Android-style removal of all system UI. Use the Home Screen app for this browser-free reading experience rather than relying on the reader's Full screen button inside a Safari tab. If an older Home Screen icon opens as a normal webpage, remove it and add it again with Open as Web App enabled where offered.

Desktop

Browser Install
Chrome / Edge on Windows, Linux or macOS Use the install icon in the address bar or the browser's Install menu item. Launch the reader from its installed app icon.
Safari on macOS Sonoma or newer Use File → Add to Dock, then launch it from the Dock.

Android install icons

The install manifest uses PNG icons with explicit 192×192 and 512×512 sizes, with separate maskable icons for Android's adaptive icon shapes. It does not offer the SVG favicon to the installer, avoiding an Android WebAPK compatibility issue with scalable icons. Apple home-screen icons are unchanged. After pulling an updated reader image, recreate the container and reload the reader before retrying installation so Android fetches the current manifest.

Passwords and Cloudflare Access

If the reader has its own READER_PASSWORD, sign in when prompted. The reader exempts only its bundled manifest, app icons and pinned-tile metadata from that password, so the operating system can fetch those install assets. The reader page, library and API remain password-protected. That exemption does not bypass authentication imposed by a reverse proxy in front of the reader.

Cloudflare Access can get in the way of installing the app in Chromium browsers on Android, even after you have authenticated and can read comics. The browser's manifest request may not carry the authentication needed by Access and can receive an Access login page or denial instead of the manifest. Safari on iOS authenticates the manifest request correctly in the same setup, so installing on an Access-protected iPhone can work even when Android installation fails.

This is not a private-network or TLS problem: a public domain with Cloudflare-provided HTTPS can still hit this authentication difference. If Android offers only a shortcut or no install option, check whether /manifest.webmanifest and the PNG icons are being returned as the actual install assets, not an Access login page. For remote Chrome DevTools, Application → Manifest and the Network panel show these requests. Do not disable protection for the reader or API just to try installation. No Cloudflare-specific authentication code is required or added by the reader.

Installing does not enable offline reading. The server and its library must still be reachable; there is no service worker downloading comics or queuing progress changes while disconnected. The browser's existing page cache and prefetch still make nearby page turns fast. There is no push notification permission or background download to enable.

The install metadata and icons are bundled with the reader's built web page. To deploy changes to them, rebuild and recreate the reader container, or pull a newly published reader image and recreate it. A restart alone cannot rebuild the frontend. For a source install, run npm run build in reader/web and restart the reader server after backend changes. No new runtime dependencies are needed. Installed apps use the current reader when reopened; a device may keep an old home-screen icon until it refreshes or the app is removed and added again.

Developing

cd reader
pip install -r requirements-dev.txt
python -m pytest            # the server, offline, a few seconds
cd web && npm install
npm test                    # spreads, taps, keys, shelves
npm run dev                 # the page, against: READER_LIBRARY=... python -m comicreader

The reader's tests run apart from the scraper's. reader/tests/test_contract.py checks what the reader relies on comiclib for, so run it after changing comiclib.