Skip to content

Release v1.7.5 - #248

Merged
Gnathonic merged 24 commits into
mainfrom
develop
Jul 5, 2026
Merged

Release v1.7.5#248
Gnathonic merged 24 commits into
mainfrom
develop

Conversation

@Gnathonic

Copy link
Copy Markdown
Owner

[1.7.5] - 2026-07-05

Added

Fixed

Changed

Gnathonic and others added 24 commits June 14, 2026 13:17
…ert)

Renaming a cloud-backed volume MOVED its .mokuro sidecar but never updated
the title/series fields embedded inside it. On re-download to another
device, processing.ts trusts the .mokuro's internal metadata over the
filename, so the rename silently reverted for OCR volumes (and series
grouping broke). The problem is provider-agnostic, so the fix lives in the
shared unified-cloud-manager.

renameVolume(old…, new…, volumeUuid) now:
- regenerates the .mokuro from the DB with the NEW names via overrides
  (generateVolumeSidecarsFromDb gains a {seriesTitle, volumeTitle} arg) —
  the DB still holds the old names because the remote rename GATES the
  local commit (handleSave already runs the cloud rename before the DB
  update, and a throw here skips it);
- uploads the fresh .mokuro at the new path, MOVES the cbz + cover (their
  content is name-agnostic), then deletes the stale .mokuro LAST;
- is idempotent (an already-moved source / already-gone target is treated
  as done) and destructive-last, so a partial failure leaves the old files
  intact and a retry converges — no journal needed;
- throws on a read-only provider (never desyncs local ahead of remote) and
  throws SIDECAR_REGEN_FAILED if an OCR volume's sidecar can't be rebuilt
  (volume_ocr missing) before touching anything remote;
- best-effort prunes the emptied old series directory via a new optional
  provider.removeDirectoryIfEmpty — server-checked emptiness only, never a
  blind recursive delete.

WebDAV renameFile is now idempotent on retry (destination occupied by the
already-moved source = success, matching Drive/MEGA), so a re-run after a
dropped MOVE response converges instead of throwing TARGET_EXISTS.

Edge cases from adversarial review folded in. Follow-up: renameSeries (bulk
folder rename) has the same latent staleness and needs the same treatment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
renameSeries now fans out the per-volume rename for OCR series so each
.mokuro's embedded series title is regenerated (a bulk folder move alone
left them stale, silently reverting the rename on re-download). Reuses the
idempotent, destructive-last renameVolumeFiles core so a partial failure
converges on retry; image-only series still take the provider-optimized
folder move. The remote rename gates the local commit — when the cloud
can't be updated (offline, read-only, …) the local rename is aborted and
the user is told why (snackbar / inline error), without surfacing the
low-level cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sanitize series/volume titles at their source (rename + import) so the
stored title is safe as a file/folder name on every sink — MEGA, Drive,
WebDAV, OneDrive, the File System Access API, and local export. Fullwidth
look-alike substitution for the Windows-illegal set; going-forward only;
no unified-cloud-manager changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sanitize the volume title on every save and a newly-created series name.
Existing-series titles are left untouched here (series-wide sanitization
happens in the series-rename flow) to avoid splitting a series across two
cloud folders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repo's default workflow token is read-only and release.yml declared no
permissions, so the Release job 403'd pushing the tag / creating the release
— every release had to be made by hand. Scope contents:write to the job.
Together with the release-notes extraction fix, releases now publish
automatically with notes from CHANGELOG.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…me ordering

Review findings on the rename flow, all pre-destructive-step:

- Replace '404'/'not found' message sniffing with typed ProviderError
  NOT_FOUND thrown at each provider boundary (WebDAV/MEGA/Drive rename +
  delete). A NOT_FOUND during a MOVE is now a genuine failure that aborts
  before the stale-.mokuro delete — an already-moved file is simply absent
  from the fresh source listing, so no error-swallowing is needed for retry
  convergence. Deletes still converge on NOT_FOUND (absence IS the
  postcondition) and drop the stale cache entry.
- WebDAV renameFile's already-at-destination branch now requires the source
  to be gone AND the occupant to match the source's size (identity proxy,
  paralleling Drive's id / MEGA's nodeId checks) instead of adopting any
  occupant.
- Collision gate before any remote write: renaming onto an occupied volume
  name throws TARGET_EXISTS before the .mokuro upsert could corrupt the
  occupant's sidecar. Explicit overwrite option deletes the occupant first.
- READ_ONLY gate moved after the zero-cloud-files check so a read-only
  provider (anonymous session, auto-demoted server) no longer blocks
  purely-local renames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rite prompt

- executeRenameSeries now commits each volume locally ONLY after its cloud
  rename succeeded, collecting per-volume failures instead of aborting the
  whole series mid-loop with a false 'kept in sync' message. Failed volumes
  keep the old title everywhere; retrying the same rename converges on just
  the stragglers. SeriesView navigates to the new series on full success and
  reports per-volume failures otherwise.
- Series rename is blocked with a download-first notice when the cloud holds
  volumes missing from the local library — renaming around them would split
  the series across two cloud folders.
  TODO(data-update): the proper fix is downloading a volume's .mokuro/
  metadata without the full archive (blocked on the metadata-persistence
  work; see PR #201).
- The rename volume list now comes from the preview (which carries
  volumeTitle) instead of re-running the same Dexie query, closing a
  divergence window between the cloud rename list and the local commit list.
- VolumeEditorModal offers overwrite-or-cancel when the new volume name
  collides with an existing cloud backup (TARGET_EXISTS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…th identity

Sanitizing titles during a cloud download broke the stored-title ===
cloud-path identity for legacy backups whose names contain now-illegal
characters (':' etc. are legal on Drive/MEGA/most WebDAV): the volume read
as un-backed-up (duplicate uploads) and renames found zero managed files —
committing locally while the stale cloud sidecar survived, the exact silent
revert this branch fixes.

Cloud downloads now save titles verbatim (preserveTitles); sanitization
still applies to local file imports and at rename time, where the rename
machinery moves the cloud files together with the title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…de it binary)

The spec embedded literal \x00/\x1F/\x7F control bytes where it meant the
printable escape notation, so git treated the file as binary. Prettier
formatting applied to the now-text docs and the sanitize-title test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…older prune

Series rename on MEGA left the old folder behind and showed the renamed
series as un-backed-up until a page refresh. Root causes:

- megajs applies moves/deletes to its local tree ONLY via the server-change
  (sc) long-poll, which we ran with keepalive:false — so storage.files went
  permanently stale after every move, and the full cache rebuild inside
  deleteFile clobbered the manager's correct incremental updates with that
  stale tree.
- The reason keepalive was disabled ('sc handler crashes on delete events')
  was self-inflicted: we manually removed nodes from storage.files, so the
  later sc delete packet found no node and crashed reading .parent.

The fix restores the intended architecture — listen to MEGA and update the
GUI reactively:

- Remove the manual storage.files removals (deleteFile, uploadFile replace)
  and re-enable keepalive on login and session restore.
- Subscribe to megajs add/move/delete/update events; a debounced rebuild
  from the now-authoritative tree keeps the reactive cache (and the GUI that
  subscribes to it) current, including changes made from other devices. The
  manager's incremental updates still give instant feedback for our own ops.
- Drop the mid-operation fetch(true) rebuilds (upload/delete/deleteSeries)
  that raced the sc stream.
- Mirror moves into the local tree immediately after moveTo (the sc handler
  no-ops when the parent already matches) so same-operation checks are
  accurate.
- Implement removeDirectoryIfEmpty for MEGA: emptiness is confirmed with a
  live 'f' API query (never the possibly-stale local tree, never a blind
  recursive delete), so series renames now prune the emptied old folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the optional SyncProvider.removeDirectoryIfEmpty() for Google
Drive, matching the MEGA/WebDAV implementations: locate the series folder
under mokuro-reader, verify emptiness with a live files.list children
query (never the cached listing), and delete only if the server confirms
the folder is empty — never a blind recursive delete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(sync): regenerate .mokuro on cloud volume rename (stop silent revert)
…full rename

The prune was gated on the manager's local cache being empty for the old
series — but the debounced provider-event rebuild can fire mid-rename and
transiently repopulate old-path entries (MEGA's tree doesn't reflect a
delete until the sc packet lands), so the gate skipped real prunes and the
emptied folder was left behind.

The provider's removeDirectoryIfEmpty already verifies emptiness against
the SERVER, so the cache gate was both racy and redundant. Prune is now
attempted unconditionally (provider decides), and once per rename instead
of per volume: after the single-volume rename in renameVolume, and after
the whole fan-out in renameSeries — safe even with partial failures, since
the server check sees the failed volumes' files. MEGA logs when the server
still reports contents, for diagnosability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: design spec for lines_coords-based original mode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(reader): per-line layout module from mokuro lines_coords

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(reader): original mode renders each line at its lines_coords quad

mokuro's block font_size is the detected line-quad width (furigana
included), median 1.2x the true character size, so block-level original
rendering overflows. Per-line quads are accurate: place each line at its
quad and fit the font size to quad_length / text_advance, capped by quad
width. Falls back to the legacy path when lines_coords is missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): center lines in their quads and drop duplicate blocks

Quads are often wider than the glyph column (ruby, mask slack, empty
margin) with the base glyphs near the middle; left-edge anchoring shoved
wide-quad columns into their neighbors (Dr Stone 01 p27 本物から).
Center each line on the quad's cross axis instead.

The detector also emits ~19 duplicate blocks per 1000 pages: the same
balloon as a properly segmented block plus a single whole-box synthetic
line with a huge font_size (Dr Stone 01 p29). Dedupe at render time by
identical joined text + box IoU > 0.5, keeping the better-segmented
block; applies to all font modes and already-imported volumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(reader): wrap merged-column lines at the block reference size

A quad much wider than the block's typical line size whose text only
fits far smaller is multiple print columns captured as one OCR line —
usually base text plus its furigana reading (Dr Stone 01 p32
空は私ならだいじょうぶ: 112px quad squeezed to 14.7px). Such lines now
wrap inside their full quad bbox at (near) the block's median line size.
Clean lines keep their own fitted size: print mixes sizes within a
balloon (emphasis words like 大丈夫), so uniformity is not forced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(reader): uniform font size across all lines of a block

Print keeps one size per balloon; per-quad fitted sizes vary only via
quad slack, and mixed sizes look sloppy (user feedback: Dr Stone p32
大丈夫 is 1:1 with the base line, its tall quad is just loose). All
lines now render at the block's median clean-line size, tolerating
1.15x length / 1.2x cross quad slack. Escapes: deliberately-small lines
(standalone furigana) keep their own size, and wrapped merged-column
lines pull the whole block down with them so sizes stay uniform.
wrapFitSize now searches column counts exactly instead of iterating to
a suboptimal fixpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): gate wrap on achievable benefit, trust clean-line consensus

The fixed 1.6x-reference quad-width wrap gate missed Dr Stone 01 p53
必要なことはう by 0.7px, leaving a merged line at a tiny 19px when two
columns at 31.5px fit. Gate on what wrapFitSize actually achieves
(>= 1.25x the single-line size) instead. And when >= 2 clean lines
agree within 1.25x, their median fixes the block size — a wrapped
line's lower fit no longer drags well-attested blocks down (it still
does for 0-1 clean-line blocks, which is what makes p32 land on the
true size).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(reader): wrap whole-balloon single-line blocks at geometry-optimal size

A block whose only line is one quad covering the whole balloon (Dr
Stone 01 p87: 9 chars in 356x390) could never wrap: the reference size
derives from that same line, so the shrink gate never fired, leaving a
small single column in a huge box. When a block has no clean lines,
suspect lines now wrap at their geometry-optimal size (~sqrt(main x
cross / advance)) — p87 renders 3 columns at ~119px matching the print.
Still gated on the 1.25x benefit so genuine one-liners in loose quads
stay single. Hallucinated lines now wrap into dense contained columns
instead of rendering sub-pixel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): weight the block size vote by quad ink area

Killing Bites 01 p42: 「百獣王」 with its katakana gloss split into two
small ruby lines around it — a plain per-line median is ruby-dominated
and dragged the 76px base text down to 31px. Lines now vote for the
block reference size proportionally to their quad ink area (the base
line holds 80% of the block's ink), so ruby fragments register as small
outliers and keep their own sizes while the base renders at its true
size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(reader): per-line lines_coords layout becomes the auto mode default

The reconstruction supersedes the old measure-and-shrink auto mode and
is not a faithful raster either, so auto (the app default) is its
honest home. Blocks without lines_coords fall back to the legacy
hover-fit auto path; original mode reverts to raw font_size rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): hide overlapping re-captured lines within a block

Saki the Succubus 02 p129: the detector re-captures the same ink region
as several overlapping lines — a column alone AND a bigger quad
spanning it plus its neighbors (あれは inside あれはキスではないですよ),
or nested hallucination blobs — which rendered stacked text. When one
line bbox covers >= 70% of a smaller one: if the smaller's text is
contained in the bigger's, hide the smaller (the bigger wraps the full
region, no text lost); otherwise hide the enclosing blob and keep the
precise small captures. Hidden lines don't vote on the block size and
don't render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): partition diverged overlap clusters into readable bands

Hiding the enclosing blob of a hallucination cluster (Saki 02 p129)
filtered out most of the OCR text — unreadable in a new way. The user
requirement is that OCR text stays readable even when it is wrong:
diverged overlapping captures now merge into a cluster whose union bbox
is partitioned into reading-order bands weighted by text length; each
line wraps inside its own band. All text renders, ordered, nothing
stacks. Text-subsumed duplicates (あれは) still collapse losslessly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): per-line white backing on lines, not the whole box

Killing Bites 01 p211: the detector assigned the middle column of one
balloon to the neighboring block (its quad slots exactly between the
victim block's columns). Per-line placement renders it at the correct
print position, but the overlapping block's whole-box white background
painted over it, hiding the text. In per-line mode the opaque backing
now lives on each line span — it still masks the printed glyphs under
the rendered text, and overlapping boxes can no longer blank out each
other's lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reader): paint all box panels below all per-line text

Per-line white strips only mask print they cover — on stylized, angled
or misaligned text (Saki 02 p129) the original ink bleeds around every
strip. Restore the whole-box white panel and fix the actual p211
problem, which was paint order: text boxes no longer create stacking
contexts (z-index removed; tree order still stacks smaller boxes over
bigger), and positioned line spans get z-index 1, lifting every block's
text above every block's panel. Overlapping blocks mask robustly and
can no longer blank out each other's lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: mark line-coords spec as shipped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Restores z-index:11 on the box and its panel and drops the
positionedLine z-index lift from f6b2aef.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mokuro-reader Building Building Jul 5, 2026 6:55pm
mokuro-reader (develop) Ready Ready Preview, Comment Jul 5, 2026 6:55pm

Request Review

@Gnathonic
Gnathonic merged commit 0ec642c into main Jul 5, 2026
14 checks passed
adrian-tompkins pushed a commit to adrian-tompkins/mokuro-reader that referenced this pull request Aug 2, 2026
…ormat-support

feat: Add extended image format support and fix URL-encoded filenames
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant