Compare commits

..
34 Commits
Author SHA1 Message Date
John O'Keefe 6aea6a4e2c fix(sync): countTextCharsBefore multiplied the document by node depth
Release / build-and-push (push) Successful in 2m57s
countTextCharsBefore started its sibling walk at the target node itself
and recursed after counting the current node's subtree, so every
ancestor level re-counted everything accumulated so far — a node at
depth 4 in the single-document 1984 epub reported 2.38M chars before it
in a 589k-char document, producing healed percentages of 4.04 (>1) and
a 500 on the reading_progress percentage check constraint. Every
healed progress save from a fresh client failed; only exact-context
matches (no heal) stored.

The walk now starts at target.PrevSibling: strictly the characters
before the node, per the contract all three callers already assume
(healed book offsets, CRE convert percentage, kepub offsets).

Found by the real-phone validation pass: the phone's first healed
submission 500ed where the emulator's had matched context exactly and
never taken the heal path.
2026-09-26 22:01:35 -04:00
John O'Keefe b10bf3e8c7 docs(api): real progress endpoint contract; remove nonexistent-endpoint docs
The progress documentation described GET/POST /api/progress/:id —
endpoints that do not exist in the router — while the endpoint every
client actually uses (GET/PUT/DELETE /api/media-items/:id/progress) had
no field-level docs at all.

New:
- progress/position-contract.md: the canonical position model — server
  as position authority, the three-tier submission (percentage /
  context_text / epubcfi), ingest verification and healing, the restore
  handles, the OPF spine numbering hazard (canonical CFI spine steps
  include linear="no" items; clients resolve documents by anchor_href
  and land by css_selector + char_offset, never by spine step), the
  offset currencies (UTF-16 at the wire, runes internal), and
  context_text rules.
- progress/get_media_progress.md and update_media_progress.md: the real
  endpoints with full field tables, conditionality of the restore
  handles, the first-page anti-clobber guard, and healed-response
  semantics.
- progress/delete_media_progress.md: the real DELETE route.
- koreader/sync_progress.md and koreader/get_metadata.md rewritten to
  the actual payloads: the plugin sends a single-book array whose
  "epubcfi" field is a CRE xpointer; the metadata response navigates
  via koreader_xpointer (canonical CFI converted back to CRE), with
  page as the canonical locator for fixed-layout books.

Removed: the five files documenting the nonexistent /api/progress/:id
GET/POST/DELETE endpoints. Kept get_progress_history.md (that route
exists). The legacy developer/api-reference.md and the indexed
api/api-reference.md progress sections now match the wire and link the
new docs; the duplicate "Universal Progress" section points at Reading
Progress.
2026-09-26 21:27:44 -04:00
John O'Keefe 8c3273a0fc fix(sync): normalize character offsets to UTF-16 at the wire; refresh book offset on every verified save
Offset currency policy, now explicit: EPUB CFI terminals, CRE text()
offsets and the served char_offset handle are UTF-16 code units (the
EPUB CFI spec, and what foliate/readium/KOReader/Kobo clients actually
observe), while internal arithmetic — the book-wide character_offset
column and percentage fractions — stays rune-based, consistent with
TotalCharacters. For all-BMP books the currencies are identical, so no
stored value changes; astral-plane text (emoji, rare CJK) no longer
drifts.

Boundaries converted: resolveCFIToNode interprets incoming CFI terminal
offsets as UTF-16; textNodeAtUTF16Offset (née textNodeAtRuneOffset)
interprets CRE text() offsets as UTF-16; buildCFI and buildCREXPointer
emit UTF-16 terminals; blockCharOffset (the served char_offset) is
UTF-16.

Also fixes two character_offset column defects: heals wrote a BLOCK-
relative offset into the book-wide column, and verified-but-unhealed
saves (e.g. KOReader pushes) never refreshed it, leaving it stale
behind the anchor. VerifyProgressAnchor now returns the verified book-
wide rune offset and SaveProgress refreshes the column on every
verified save.

Tests: astral currency round trip (offset after an emoji must shift by
one unit between currencies, in both heal and exact-verify directions)
and book-offset ordering. The cmd/server/tests integration harness
failures under docker (library folder 400 during setup) reproduce on
the pre-change tree and are unrelated.
2026-09-26 20:18:48 -04:00
John O'Keefe aec226af1a feat(sync): server-side position authority — verify/heal progress anchors
Progress submissions now carry (percentage, context_text, epubcfi) and
the server becomes the position authority:

- VerifyProgressAnchor resolves the submitted standard CFI against the
  book's own XHTML, extracts the text at the anchor, and cross-checks it
  with the submitted context_text. A mismatch heals the anchor by text
  search (percentage disambiguates repeats) instead of storing a bad
  position.
- The anchor's block element is derived as a cssSelector plus a block-
  relative character offset, and served on progress GET alongside the
  anchor document's href — readium-native handles that let clients
  re-open a book without parsing CFIs themselves.
- context_text-only submissions (no CFI — the dumb-client tier) are
  anchored structurally from the context text.

Motivation: cross-client progress sync (web foliate CFIs, KOReader CRE
xpointers, readium-native apps) previously trusted each client's own
locator math; the app's EPUB restore drifted ±pages because readium's
paginator does not lay out far-from-viewport columns and the foliate-
ported CFI walk ran against readium's mutated WebView DOM. Server-side
verification heals both classes at ingest.
2026-09-26 14:43:34 -04:00
John O'Keefe 1c1f5cf1bb feat(version): bake release tag into binary; About card and /health field
Release / build-and-push (push) Successful in 3m1s
The version tag previously existed only in git and the Docker image tag —
the running app had no way to report what build it was. Version is now
injected at build time via -ldflags into internal/version.Version (defaults
to "dev" for local builds), passed by the release workflow as the
APP_VERSION Docker build arg from the pushed tag.

Surfaced in Admin → Settings → About (extensible card for future rows like
disk space) and in the /health JSON response, so deployments can be
verified with curl alone.
2026-09-21 20:21:43 -04:00
John O'Keefe 9e4cc4efd0 chore(templates): regenerate templ outputs and rebuilt static assets
Release / build-and-push (push) Successful in 2m42s
Regenerates all *_templ.go files after the templ generate pass (version
stamp updates across the board; functional changes only in
admin_archived_templ.go and admin_library_templ.go, which were committed
with their features). web/static/style.css and web/static/htmx.min.js are
refreshed outputs of npm run build (tailwind pass over the current
templates; htmx copied from the current node_modules version).
2026-09-21 18:10:47 -04:00
John O'Keefe 7b0b2793a6 test(infra): self-healing admin password; cross-library move/merge suite
setupTestServer reuses the shared dev admin (testuser@tests.bookhoard.internal)
instead of re-inserting it, but never reset its password — once any test
mutated the admin's password, every later test in the run failed to log in
with 401s until the database was manually wiped. The seeding step now
resets the password hash to the known test constant on reuse, so full
integration runs are repeatable against an existing database.

Adds cross_library_move_test.go, seven integration tests covering the
duplicate-content features end to end:

- TestCrossLibraryMovePreservesHistory: a book moved between two same-type
  libraries keeps its row ID — progress and annotations survive, no
  duplicate, no archived ghost, library_type_name stays truthful.
- TestCrossLibraryCopyStaysIndependent: deliberate copies in two libraries
  stay independent rows with isolated progress.
- TestCrossLibraryMoveRejectedForTypeMismatch: a reflowable EPUB is not
  repointed between manga libraries; a library_type_mismatch processing
  issue is recorded instead.
- TestListHiddenMediaItemsMatchesActiveTwin: archived rows expose their
  active same-SHA twin (and rows without one report no match).
- TestMergeArchivedItemIntoActiveTwin: merging moves progress and
  annotations onto the active copy and removes the archived row.
- TestMergeArchivedItemRejections: non-admin 403, active source, hash
  mismatch, missing target_id, self-merge, and archived target all fail
  closed.
- TestLibraryTypeNameTriggerOnLibraryChange: the UPDATE trigger refreshes
  library_type_name when a row changes libraries.

Tests create their own temp-dir library folders (host runs don't have
/app/uploads), use unique device identifiers (device_identifier is
UNIQUE and leftover rows broke reruns), and clean up via an explicit
defer that runs while the pool is still open — a t.Cleanup registered
for the same purpose silently no-ops because it executes after
setup.Close has closed the pool.
2026-09-21 18:10:37 -04:00
John O'Keefe 077afaca56 chore(docker): bake Go module cache into the test-runner image
The test-runner stage copied only /app from the builder, but the Go module
cache lives in /go/pkg/mod (GOMODCACHE) — the cache mounts used by the
builder target /root/go/pkg/mod, which the go tool ignores, so those mounts
never held anything. Every `compose run tests` / make test-integration
invocation therefore re-downloaded all dependencies from the network,
making integration runs slow and timeout-prone.

The builder now materializes /go/pkg/mod into an image layer
(go-module-cache) after the binary build, and the test-runner stage
restores it to /go/pkg/mod before running tests. Test-only stage; the
production final stage is unaffected.
2026-09-21 18:10:17 -04:00
John O'Keefe 388b62b277 fix(admin): delete-library warning names what is lost; modal now closes
Three fixes to the Delete Library flow on /admin/library:

1. Honest warning copy. The old text ("Media files will not be deleted")
   read as reassurance while deleting a library actually cascade-destroys
   every book record with it: reading progress, highlights, notes,
   bookmarks, and ratings are gone with no archive window and no undo.
   The modal now states this as a scannable list:
   - Permanently removed, no archive or undo: the library, its folder
     mappings, and all book records — with their reading progress,
     highlights, notes, bookmarks, and ratings.
   - Not touched: media files on disk.
   Admins skim danger dialogs; the irreversible part now leads.

2. The dialog stayed open after confirming. The confirm button swaps
   #libraries-container via htmx (so the deleted row vanished) but the
   modal lives outside the swapped container and nothing closed it. An
   htmx:afterRequest listener now hides the modal on successful deletes
   and leaves it open on errors.

3. Latent ReferenceError in openFolderBrowser: the function parameter is
   targetInputId but the body referenced an undefined targetInput, so
   opening the folder browser threw and the picker never loaded.
2026-09-21 18:09:52 -04:00
John O'Keefe 5da97b2e5d feat(opds): library folder navigation and library-scoped search
The device catalog flattened every visible library into one list, so
duplicate copies of the same book each got an entry and search had no
library context. The root feed is now a standard OPDS 1.1 navigation feed
that mirrors the web UI's library model:

- Root /catalog: an "All Books" entry first (the previous flat cross-
  library behavior, also still served at ?all=1 for clients that want
  the single flat list), followed by one folder entry per visible
  library with live book counts from GetVisibleLibraryMediaCounts.
- New GET /library/:libraryId/catalog: acquisition feed scoped to one
  visible library (403 when the device owner cannot see it), paginated,
  with an up-link back to the root.
- Scoped search: each library feed's rel="search" OpenSearch template
  pins &library_id=<id>, so opening search from inside a library folder
  searches only that library — clients substitute only {searchTerms},
  so no client-side changes are required. Root search stays global.
- Global search entries now carry the owning library as a category (and
  as a fallback summary when the book has no description), so duplicate
  copies are distinguishable in unscoped result lists.

The acquisition entry builder is extracted into addAcquisitionEntries and
shared by the all-books and per-library feeds. bookhoard.koplugin needs
no changes: it only registers the root URL, and KOReader's stock OPDS
client renders navigation feeds natively.

Tests: TestOPDSLibraryFolders covers the nav-feed shape, flat ?all=1
mode, scoped catalog isolation, scoped/global search behavior, and 403s
for libraries outside the device's visibility. Library names avoid the
word "test" on purpose — setupDeviceTest re-runs setupTestServer's
setup-time cleanup, which deletes every library whose name contains it.
2026-09-21 18:09:30 -04:00
John O'Keefe 45e3a256e2 feat(collections): merge archived duplicates into their active copy
Covers the copy-then-delete-later workflow: a user copies books to a new
library, deletes the originals, and ends up with an active copy in the new
library plus an archived twin holding the real reading history. Until now
those twins could only be purged (destroying the history) or restored
(showing a permanently broken entry).

- POST /api/media-items/:id/merge (admin only, body {target_id}):
  validates the source is archived/missing, the target is active, and
  both share the same file_sha256; then re-parents every child row onto
  the target via the existing reparent_media_item_children function (the
  same machinery as hash-conflict resolution) and deletes the source row.
  Per-user collisions keep the active copy's data, mirroring that flow.
  Affected data: reading progress, speed, ratings, highlights, notes,
  bookmarks (including tombstoned deleted-annotation history), formats,
  collections, kobo shelves/entitlements, sync rows, panel data,
  processing issues, and device aliases.
- GET /admin/archived: ListHiddenMediaItems' match_* columns surface each
  row's best active twin; rows with a twin get a confirm-guarded "Merge"
  button (data-merge-source/-target) next to Restore/Delete, wired in
  web/src/admin.ts like the existing unarchive/delete handlers.
- templates.ArchivedItem gains MatchID/MatchTitle/MatchLibraryName;
  frontend.go populates them from the listing row.

After a merge the archived row is gone, so the retention purge can never
destroy the merged data. Deleted-annotation tombstones carry over and
remain restorable from the target book's "recently deleted" history.
2026-09-21 18:09:05 -04:00
John O'Keefe 64ef691d64 feat(scanner): repoint rows on cross-library moves of identical content
Previously the scanner's SHA-256 dedup was library-scoped: moving a book
between libraries created a duplicate row (new ID) while the old row went
missing and archived after two scans, orphaning reading progress and
annotations from the file that users still see.

processMediaFile now falls through to cross-library detection when the
same-library hash lookup misses:

- GetMediaItemsBySHA256AndSameLibraryType returns candidates in other
  same-type libraries; each candidate's file is stat'd through ITS OWN
  library's folders (the old existence check stat'd against the current
  scan's folder tree, which is meaningless across libraries).
- File gone -> it is a move: the candidate must pass the target library's
  type rules (ValidateMediaItemForLibrary), then the row is repointed via
  MoveMediaItemToLibrary with the recomputed relative path. Reading
  history, annotations, and collections follow automatically because the
  row keeps its ID. Incompatible formats (e.g. a reflowable EPUB into a
  manga library) are rejected with a processing issue instead of being
  force-imported; the upsert on (media_item_id, issue_type) keeps
  repeated scans from spamming duplicates.
- File still present -> deliberate multi-library copy: fall through to
  normal import so both libraries keep independent rows.

Candidate selection is factored into selectMoveCandidate (pure function,
unit-tested in media_scanner_move_test.go): input arrives pre-ordered
(archived first, then most missing scans, then oldest) and only
candidates whose file is verifiably gone qualify, so deliberate copies
are never repointed.
2026-09-21 18:08:43 -04:00
John O'Keefe a4dbe79d0b feat(data): duplicate-content queries — archive filters, cross-library moves, archived-twin matching
Three related query groups in queries.sql (sqlc regenerated; no generated
signatures changed, so no caller edits were needed):

1. Dashboard/collection archive filters. The smart-section and collection
   queries returned archived and missing items (visible only as broken
   covers once their files vanished). Added the codebase-standard
   "AND archived_at IS NULL AND missing_scan_count = 0" predicate to:
   GetContinueReadingItems, GetRecentlyAddedItems, GetRecentlyReadItems,
   GetNotStartedItems, GetCollectionItemsForDashboard, GetLibraryItems.
   Matches the existing convention in ListMediaItems, SearchMediaItems,
   the library counts, and GetContinueSeriesItems.

2. Cross-library SHA move detection support.
   - GetMediaItemsBySHA256AndSameLibraryType: finds identical content
     (same file_sha256) registered in a DIFFERENT library of the SAME
     library type, ordered archived-first, most-missing, oldest. Type
     scoping keeps ebooks from merging into manga/comics libraries.
   - MoveMediaItemToLibrary: repoints an existing row to the new library
     (library_id + recomputed file_path + file_size) and clears
     archive/missing state. The row keeps its ID, so reading progress,
     annotations, collections, and kobo shelves follow the book
     automatically; the new set_library_type_name_on_library_change
     trigger refreshes the denormalized type column.

3. Archived-twin matching. ListHiddenMediaItems (admin archived-items
   page) now LEFT JOIN LATERALs the best active same-SHA twin per hidden
   row — preferring same library type, then same library, then most
   reading progress, then oldest — exposing match_id / match_title /
   match_library_name so the UI can offer merging the archived row's
   reading data into its active duplicate. COALESCE on match_title keeps
   the no-match case NULL-safe.
2026-09-21 18:08:18 -04:00
John O'Keefe 1002cbca93 fix(schema): refresh library_type_name on library change; make trigger block re-runnable
schema.sql is re-executed in full on every application startup, so every
statement in it must be idempotent. Two related problems introduced with the
cross-library move feature, plus their fix:

Problem 1: media_items.library_type_name was only populated by a BEFORE
INSERT trigger. When the scanner repoints an existing row to a different
library (cross-library move detection), the denormalized library_type_name
went stale, mislabeling the item's type for validation and display.

Fix: add a second trigger, set_library_type_name_on_library_change, that
fires BEFORE UPDATE OF library_id and re-runs the same population function.

Problem 2 (outage): the new trigger EXECUTE FUNCTIONs set_library_type_name(),
but the pre-existing idempotency dance drops that function on every startup
before recreating it. On the second and later boots, DROP FUNCTION failed
with SQLSTATE 2BP01 (function still depended on by the trigger created by
the previous boot), aborting the whole schema transaction and crash-looping
the container.

Fix: drop BOTH triggers before dropping the function, and create both after
it. The ordering now survives any number of restarts on any database state.

Adds TestSchemaInitializationIsIdempotent, which replays the production
database.Initialize twice against the same database so this class of
"second startup" regression fails in CI instead of in production. Note the
test uses a dedicated pgxpool: Initialize holds the advisory lock on one
connection while executing the schema on another, which deadlocks against
the shared test pool's MaxConns=1.
2026-09-21 18:07:51 -04:00
John O'Keefe 65f6bdf075 chore: complete go.sum with test-graph dependency sums
go mod download all added sums for modules only reachable from test
builds (chroma, bluemonday, compress families); without them the
integration-test binary build fetches missing sums at compile time.
2026-09-18 18:09:20 -04:00
John O'Keefe 4faa217822 test: repair integration harness — wire SettingsRegistry, reuse preserved dev admin
Two pre-existing harness breaks found while running the device tests:

- RegisterRoutes dereferences cfg.Settings (auth rate limit) but
  setupTestServer never set it — every integration test nil-panicked at
  route registration. Wire database.NewSettingsRegistry(queries) the
  same way main.go does.
- The cleanup deliberately PRESERVES testuser@tests.bookhoard.internal
  (shared dev admin), but setup then blindly re-INSERTed that user, so
  every run after the first failed on users_email_key. Reuse the user
  when it exists; default collections are created only for a NEW user
  (the preserved admin already has its set).

Also documented the offline runner used on slow links: build the test
binary on the host (CGO_ENABLED=0 go test -c) and execute it inside the
bookhoard-tests image against the compose network — no in-container
module downloads, no image rebuild.
2026-09-18 16:06:14 -04:00
John O'Keefe 5cdfe5a8dd fix(devices): release pendingMu on the pending-status path of CheckRegistrationStatus
The mutex hardening added a fall-through 'pending' response path that
returned while still holding pendingMu — one status poll before approval
leaked the lock and every later register/approve/status request hung
forever (caught by TestDeviceRegistrationFlow's approve step hanging).
2026-09-18 16:06:03 -04:00
John O'Keefe 1cd8557b58 fix(devices): re-approving a known device rotates its token instead of 500
App reinstalls that preserve data (Android Studio installDebug over an
existing install) re-register with the same device_identifier, but the
devices row from the previous install still exists — device_identifier
is UNIQUE, so ApproveDevice's blind INSERT failed with a unique
violation and returned 500 'failed to create device' (reproduced via
curl: second approve with the same identifier = instant 500; the ~98s
in the original report was app-side retry/polling, not server wait).

ApproveDevice is now idempotent: look the device up by identifier
first; a row owned by the approving user gets its auth token rotated
via UpdateDeviceAuthToken (row id unchanged, so synced highlights/
bookmarks/progress anchored to it stay valid; fresh install = fresh
credentials, old token invalidated); a row owned by another user gets
409; unknown identifiers INSERT as before, with the 23505 race falling
through to the rotate path. DB failures are logged (they were silent).

Also guard the in-memory pendingRegistrations map with a mutex —
register/approve/reject/status/list all touch it from HTTP goroutines,
and a racing write is a Go runtime fatal, not an error. The approver's
credential publication and the status poller's approved-branch snapshot
now run under the lock so the token can never be read half-written.

Regression test: TestApproveDeviceReapprovalRotatesToken — register →
approve → re-register same identifier → approve (must be 200) → token
rotated, exactly one devices row, row carries the new token.
2026-09-17 23:09:02 -04:00
John O'Keefe b4c956aed4 docs(user): web reader guide — navigation, touch selection, highlights
User-facing documentation for the rebuilt web reader (§6.5 of the app
handoff): swipe-to-page / tap-for-menu touch navigation, long-press word
selection with drag extension and handles, the selection popover (color
dots, notes with save-together-on-create, copy incl. plain-HTTP LAN,
edit/delete by tapping a painted highlight), below-the-selection popover
placement rationale, PDF/comic behavior, and annotations drawer.
Linked from the user documentation portal and the docs index quick
links / quick-find tables.
2026-09-17 20:13:45 -04:00
John O'Keefe 7caa46c2da fix(build): exclude local package-lock.json from Docker build context
The lockfile is gitignored (each machine keeps its own), but
.dockerignore did not exclude it, so any stale local lock rode into
every docker build via `COPY package*.json`. Because the forked
foliate-js declares "version": "0.0.0" on every commit, npm treats
the git pin as already satisfied by name@version and never
re-resolves the new commit hash — silently installing and bundling
the old code. This bit both the host npm cache mount (documented at
Dockerfile:18-20) and, today, `make rebuild-app-force`: a fresh
no-cache image was built with the pre-feature 1305a52 foliate-js
(chunk fixed-layout-B8-qRQLl.js) despite package.json pinning
e16530a, while the Gitea runner (fresh checkout, no lockfile, cold
cache) built correctly.

With no lockfile in the context, npm install resolves git pins
fresh from package.json each build (tarballs are cached by
commit-specific URLs), so the persistent npm cache mount cannot
serve old commits across pin bumps. Local lockfiles can no longer
poison builds even if regenerated on the host.

Verified: after evicting the poisoned cache mounts
(docker builder prune --filter type=exec.cachemount) and rebuilding,
the container serves fixed-layout-BE0KdOql.js with both dblclick
handlers present, matching the reference build.
2026-09-17 15:37:58 -04:00
john-okeefe 5329bf35a9 fix: pin foliate-js e16530a — desktop double-click zoom toggle
Release / build-and-push (push) Successful in 3m20s
Bump the linuxhg mirror pin from 1305a52 to e16530a (mirror main is
already synced). The fork adds a desktop double-click zoom to the
fixed-layout renderer for all fixed-layout content (comics, manga,
PDFs, fixed EPUBs):

- at fit scale, double-click zooms to 2.5x at the clicked location
- zoomed or panned, double-click resets to 100% centered
- PDF text-layer spans and annotation links keep native double-click
  (word selection) in Smart/Text modes; Pan mode zooms anywhere
- mirrors the existing touch double-tap; disabled in panel-zoom mode
  like the other gestures

Also closes the gap where node_modules was serving a stale tarball
older than the previous 1305a52 pin (missing the toolbar-resize
debounce and selection-drag fixes); a fresh install now resolves the
full set.
2026-09-17 14:37:05 -04:00
john-okeefe 80e5d5b0d1 feat(reader): comic treatment for fixed-layout items in comics/manga libraries
PDFs shelved in comics or manga libraries (scanned manga, official manga
PDFs with text layers) now get comic reader treatment: bookmarks only —
no text-selection highlights, no annotations, no in-book search. Items
in ebooks libraries are completely unaffected, and EPUBs keep their
existing behavior everywhere.

- handlers: new exported ShouldTreatAsComic(libraryType, formatGroup) —
  true for fixed_layout/comic_archive in manga/comics libraries; the
  reader JSON API also exposes it as treat_as_comic
- router: the SSR reader page fetches the library type and passes
  TreatAsComic through ReaderMetadata into the init config
- reader: when treatAsComic is set, the PDF textLayer selection listener
  (mouse and touch paths) never attaches so no highlight popover can
  open; pointer mode is forced to pan (Smart/Text segment hidden);
  search button and toggleSearch are disabled; previously-created PDF
  highlights stop rendering
- bookmarks are unchanged (already page-index based for fixed layout),
  and device sync / OPDS / format classification are untouched since
  format_group stays fixed_layout
2026-09-17 14:36:58 -04:00
John O'Keefe b3a429d7e1 fix(reader): Save/Cancel in the note editor closes the whole popover
Release / build-and-push (push) Successful in 2m46s
Cancel only collapsed the editor section, and edit-mode saves only set
noteOpen=false — the minimized popover (colors/pencil/copy/delete) stayed
up. Explicit Save and Cancel now call hideSelectionPopover; color tweaks
with the editor open still keep it open.
2026-09-17 11:47:25 -04:00
John O'Keefe d8179bb066 fix(reader): popover dismiss on PDF taps; notes save on create; colors don't close editor
- The in-book pointerdown dismiss listener lived in the reflowable-only
  block, so PDF taps never closed the selection popover — hoist it so
  every content doc (EPUB, PDF, comics) dismisses on tap.
- createHighlight hardcoded note_text: '' — 'Highlight with note' saved
  the highlight but silently dropped the note. Send p.note (and pass it
  to the EPUB overlayer).
- saveHighlightChanges collapsed the note editor on every save; color
  dots in create mode created a highlight outright. With the editor
  open, color dots now just recolor (create) or recolor-and-save with
  the editor kept open (edit).
2026-09-17 10:55:21 -04:00
John O'Keefe 960305dfa9 fix(reader): PDF selection popover shows reliably on touch, below selection
Chromium's touch selection takeover swallows pointerup in the fx iframe,
so the PDF popover's only trigger never fired on real phones — it only
appeared when timing happened to deliver the event. Mirror the EPUB
path: a selectionchange debounce (400ms settle) opens the popover, with
the pointerup path kept for desktop plus the quick-tap word-select guard.

Also place the popover BELOW the selection on coarse pointers (+90px
clearing the native selection menu and drag handles), matching the
reflowable path; desktop keeps above-placement.
2026-09-17 10:40:12 -04:00
John O'Keefe 16bbeec5a3 fix: fresh npm cache mount id to evict stale foliate-js tarball 2026-09-17 10:17:34 -04:00
John O'Keefe 39f9c39360 fix: pin foliate-js 1305a52 via linuxhg mirror
GitHub-pinned builds silently reused a stale foliate-js tarball from
Docker's npm cache mount (name@version never changed from 0.0.0);
every build since the GitHub pin shipped pre-94bb384 code. Repin to
the mirror and bust the cache mount.
2026-09-17 10:13:33 -04:00
John O'Keefe 011b6d14e4 fix: pin foliate-js 1305a52 — fx renderer ignores toolbar resizes during selection
The fx renderer re-rendered the PDF on every viewport resize;
mobile browser toolbar transitions (7-17% height change) during a
text selection drag caused the old canvas to be cleared for
re-rendering while the async pdf.js render raced with the next
resize, blanking the page. Now gated by the same 25% threshold
as the paginator.
2026-09-16 17:30:00 -04:00
John O'Keefe d4661e7f04 fix: pin foliate-js 94bb384 — PDF selection drags no longer blank the page
The fx renderer's gesture classifier only checked whether the touch
started on a .textLayer span; Chromium's long-press selects the
nearest word even when the finger landed between spans, so the
classifier saw a non-selectable target and classified the drag as
swipe/pan — preventDefault on the iframe's touchmove then cancelled
the native selection extension mid-drag and could blank the PDF
canvas. If any frame already has a non-collapsed selection, the
drag is now classified as native.
2026-09-16 17:13:24 -04:00
John O'Keefe 27a93ba2ea fix(reader): hide the chrome when a touch selection starts
Release / build-and-push (push) Successful in 2m28s
pokeChrome's anySelection gate only prevents the chrome from being
raised — if it was already visible when the selection began, it
stayed up. noteSelectionActivity now actively hides it the moment
a selection appears.
2026-09-15 20:20:34 -04:00
John O'Keefe 7afad5dcde chore: point foliate-js at linuxhg mirror main (8a20399) 2026-09-15 20:13:07 -04:00
John O'Keefe 929a862e74 feat(reader): touch selection handles, copy fallback, highlight tap editing
- Custom drag handles (start/end) for post-lift selection adjustment:
  the native handles are disabled with the rest of the native touch
  selection controller; these are positioned at the selection's
  boundary carets and driven through the same clamped caret mapping
- Copy button falls back to execCommand via a transient textarea on
  plain HTTP (navigator.clipboard is unavailable on LAN addresses);
  both paths clear the selection and dismiss the popover
- Only dismiss the selection popover on actual position changes in
  relocate (detail.section is a fresh object on every relocate, so
  reference comparison always saw a move); a tap's no-op relocate
  was closing the just-opened highlight edit popover
- Pin foliate-js f872a01 (synthetic click dispatch on quick taps)
  and 422e8e0 (don't snap taps that never panned)
2026-09-15 19:33:45 -04:00
John O'Keefe 0e55dafb1e fix(reader): mobile touch reading overhaul
- tap zones: map in-iframe taps into the visible page slice (the
  iframe is laid out at full section width; every tap previously
  computed as the left margin) and shrink the default zone size to 12%
- hyphens: manual on coarse pointers: Chrome's touch word-selection
  walks hyphen fragments, re-anchoring line-start drags and
  overshooting line ends into the next column
- selection popover: opens for settled touch selections (the gesture
  takeover swallows pointerup), positioned below the selection with
  on-screen clamping; creating a highlight clears the selection so
  the browser's own menu follows
- settle-time recovery: return the view to the selection's anchor
  page and clamp the selection to the visible page after the
  browser's selection auto-scroll wanders off the page grid
- overscroll-behavior: none on the reader page (pull-to-refresh
  during downward drags)
2026-09-15 08:50:17 -04:00
John O'Keefe cc8084a713 fix(reader): pin foliate-js 9cfe266
Non-touch gate on selection auto-paging (drag-selecting no longer
pages away mid-gesture) and small height-only viewport resizes no
longer re-wrap the book (mobile browser toolbar transitions).
2026-09-15 08:50:06 -04:00
93 changed files with 4961 additions and 1275 deletions
+3
View File
@@ -20,6 +20,9 @@ logs/
# Dependencies (install fresh in container)
node_modules/
# Local-only lockfile: a stale one pins old foliate-js commits into builds
# (npm reuses name@version without re-resolving the git pin)
package-lock.json
# Environment
.env
+3
View File
@@ -56,6 +56,9 @@ jobs:
context: .
file: ./Dockerfile
push: true
# Bake the version into the binary (Admin → Settings → About).
build-args: |
APP_VERSION=${{ env.TAG }}
# Publishes both the exact version (e.g. v0.2.0) and the movable "latest" tag.
# Deployments default to "latest" via ${IMAGE_TAG:-latest} in docker-compose.yml;
# pin or roll back by setting IMAGE_TAG in .env.
+21 -2
View File
@@ -15,7 +15,10 @@ RUN --mount=type=cache,target=/root/go/pkg/mod \
# Copy package files and install npm dependencies (cached unless package.json changes)
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
# npm cache mount renamed from /root/.npm: the old volume held a stale
# foliate-js tarball (name@version reuse) that --no-cache and builder prune
# could not evict; a new mount id guarantees a clean fetch.
RUN --mount=type=cache,target=/root/.npm-cache2 \
npm install
# Copy Go mod files (cached unless go.mod changes)
@@ -34,10 +37,23 @@ RUN cd templates && templ generate
RUN npm run build:css:prod
RUN npm run build:ts
# App version baked into the binary (shown in Admin → Settings → About).
# Declared after `COPY . .` so a version bump only invalidates this layer.
# The release workflow passes the git tag, e.g. APP_VERSION=v1.5.
ARG APP_VERSION=dev
# Build Go binary (cached unless Go files or generated code changes)
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -installsuffix cgo -o main ./cmd/server
CGO_ENABLED=0 GOOS=linux go build -installsuffix cgo \
-ldflags "-X bookhoard/internal/version.Version=${APP_VERSION}" \
-o main ./cmd/server
# Materialize the Go module cache into an image layer so the test-runner
# stage can restore it. GOMODCACHE is /go/pkg/mod; cache mounts elsewhere are
# ignored by the go tool, and without this copy `go test` in the tests
# container re-downloads every dependency on each invocation.
RUN cp -r /go/pkg/mod /app/go-module-cache
# Test runner stage - includes Go runtime and test dependencies
# This stage is ONLY used for running tests, never deployed to production
@@ -50,6 +66,9 @@ WORKDIR /app
# Copy everything from builder (includes compiled binary, source code, and dependencies)
COPY --from=builder /app ./
# Restore the materialized Go module cache so `go test` works offline.
RUN mkdir -p /go/pkg/mod && cp -r /app/go-module-cache/. /go/pkg/mod/ && rm -rf /app/go-module-cache
# Install kepubify for conversion tests
RUN wget -O /usr/bin/kepubify https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-linux-64bit \
&& chmod +x /usr/bin/kepubify
+3
View File
@@ -121,6 +121,9 @@ func main() {
// Create library service
libraryService := services.NewLibraryService(queries)
// The progress service verifies submitted anchors against the book
// itself — it needs to resolve library-relative file paths.
progressService.SetMediaPathResolver(libraryService)
// Sync Go AllowedExtensions into DB so API clients see correct extensions
libraryService.SyncAllowedExtensions(context.Background())
+639
View File
@@ -0,0 +1,639 @@
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"bookhoard/internal/database"
"bookhoard/internal/services"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Cross-library SHA move detection + archived-twin merge integration tests.
// These cover the scenario where identical book content moves between
// libraries of the same type: the row must be repointed (history follows)
// instead of duplicated, deliberate copies stay independent, type rules are
// enforced, and archived twins can be merged into their active copy.
const (
moveTestAdminEmail = "testuser@tests.bookhoard.internal"
moveTestFileName = "Test Book.epub"
)
// createEbooksTestLibrary creates a library of the given type via the API and
// swaps its default folder for a fresh temp directory the test controls.
func createTypedTestLibrary(t *testing.T, setup *TestServerSetup, name, libType string) (pgtype.UUID, string) {
t.Helper()
ctx := context.Background()
libBody, _ := json.Marshal(map[string]interface{}{"name": name, "type": libType})
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+setup.Token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusCreated, resp.StatusCode, "library creation should succeed")
var libResp map[string]interface{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&libResp))
libID, err := uuid.Parse(libResp["id"].(string))
require.NoError(t, err)
pgLib := pgtype.UUID{Bytes: libID, Valid: true}
folders, err := setup.DB.GetLibraryFolders(ctx, pgLib)
require.NoError(t, err)
for _, f := range folders {
_, err = setup.DB.DeleteLibraryFolder(ctx, database.DeleteLibraryFolderParams{
LibraryID: pgLib,
FolderPath: f.FolderPath,
})
require.NoError(t, err)
}
tmpDir := t.TempDir()
_, err = setup.DB.AddLibraryFolder(ctx, database.AddLibraryFolderParams{
LibraryID: pgLib,
FolderPath: tmpDir,
})
require.NoError(t, err)
return pgLib, tmpDir
}
// deleteLibraries removes libraries (cascading to media items, progress,
// annotations, and processing issues) so reruns never see leftovers from a
// previous run — stale rows with identical content hashes would otherwise be
// picked up as move candidates. Call via defer AFTER the setup.Close defer so
// it executes while the pool is still open (defers run LIFO).
func deleteLibraries(t *testing.T, setup *TestServerSetup, libs ...pgtype.UUID) {
t.Helper()
for _, lib := range libs {
_ = setup.DB.DeleteLibrary(context.Background(), lib)
}
}
// scanTestFolder runs a synchronous scan over one folder.
func scanTestFolder(t *testing.T, setup *TestServerSetup, adminID pgtype.UUID, folder string) {
t.Helper()
scanner := services.NewMediaScanner(setup.DB)
defer func() { _ = scanner.Close() }()
scanner.SetAdminID(adminID)
require.NoError(t, scanner.SetFolders([]string{folder}, false))
require.NoError(t, scanner.ScanFolders(context.Background()))
}
// waitForCond polls cond until true or the timeout elapses.
func waitForCond(t *testing.T, timeout time.Duration, msg string, cond func() bool) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("condition not met within %v: %s", timeout, msg)
}
func moveTestAdminID(t *testing.T, setup *TestServerSetup) (pgtype.UUID, pgtype.UUID) {
t.Helper()
admin, err := setup.DB.GetUserByEmail(context.Background(), moveTestAdminEmail)
require.NoError(t, err)
pgAdmin := pgtype.UUID{Bytes: admin.ID.Bytes, Valid: true}
return pgAdmin, pgAdmin
}
// writeTestEbook places an identical dummy epub under folder/Author/.
func writeTestEbook(t *testing.T, folder string, content []byte) string {
t.Helper()
authorDir := filepath.Join(folder, "Author")
require.NoError(t, os.MkdirAll(authorDir, 0755))
bookPath := filepath.Join(authorDir, moveTestFileName)
require.NoError(t, os.WriteFile(bookPath, content, 0644))
return bookPath
}
// TestCrossLibraryMovePreservesHistory is the core scenario: a book moves
// from library A to library B (same type). The existing row must be repointed
// to library B keeping its ID so progress and annotations survive, instead of
// creating a duplicate while the old row goes missing.
func TestCrossLibraryMovePreservesHistory(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, adminPgID := moveTestAdminID(t, setup)
libA, folderA := createTypedTestLibrary(t, setup, "Move Src Library", "ebooks")
libB, folderB := createTypedTestLibrary(t, setup, "Move Dst Library", "ebooks")
defer deleteLibraries(t, setup, libA, libB)
content := []byte("cross library move test epub content")
writeTestEbook(t, folderA, content)
scanTestFolder(t, setup, adminID, folderA)
var original database.ListMediaItemsByLibraryRow
waitForCond(t, 5*time.Second, "book should be imported into library A", func() bool {
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
if err != nil || len(books) != 1 {
return false
}
original = books[0]
return true
})
// Give the book real user data at its original location.
_, err := setup.DB.UpdateReadingProgress(ctx, database.UpdateReadingProgressParams{
MediaItemID: original.ID,
UserID: adminPgID,
CurrentPage: pgtype.Int4{Int32: 12, Valid: true},
TotalPages: pgtype.Int4{Int32: 300, Valid: true},
})
require.NoError(t, err)
_, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
MediaItemID: original.ID,
UserID: adminPgID,
SelectionText: "a line worth keeping",
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
require.NoError(t, err)
// Move the file: same bytes, new library folder, old copy deleted.
writeTestEbook(t, folderB, content)
require.NoError(t, os.Remove(filepath.Join(folderA, "Author", moveTestFileName)))
scanTestFolder(t, setup, adminID, folderB)
var moved database.ListMediaItemsByLibraryRow
waitForCond(t, 5*time.Second, "book should be repointed into library B", func() bool {
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libB)
if err != nil || len(books) != 1 {
return false
}
moved = books[0]
return true
})
assert.Equal(t, original.ID, moved.ID, "row ID must be preserved so history follows the book")
assert.Equal(t, libB, moved.LibraryID, "row must now belong to library B")
assert.Equal(t, filepath.ToSlash(filepath.Join("Author", moveTestFileName)), moved.FilePath, "path must be repointed relative to library B")
assert.False(t, moved.ArchivedAt.Valid, "moved row must not be archived")
assert.Equal(t, int32(0), moved.MissingScanCount, "moved row must not be marked missing")
assert.True(t, moved.LibraryTypeName.Valid && moved.LibraryTypeName.String == "ebooks", "library_type_name must stay truthful after library change")
// History followed the row.
progress, err := setup.DB.GetReadingProgress(ctx, database.GetReadingProgressParams{MediaItemID: original.ID, UserID: adminPgID})
require.NoError(t, err)
assert.Equal(t, int32(12), progress.CurrentPage.Int32, "reading progress must survive the move")
highlights, err := setup.DB.GetMediaHighlights(ctx, database.GetMediaHighlightsParams{MediaItemID: original.ID, UserID: adminPgID})
require.NoError(t, err)
assert.Len(t, highlights, 1, "annotation must survive the move")
// Library A no longer holds the row: no duplicate, no archived ghost.
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
assert.Empty(t, booksA, "library A must not keep a duplicate row after the move")
}
// TestCrossLibraryCopyStaysIndependent verifies that placing identical
// content in two libraries on purpose creates two independent rows: the
// second import is NOT blocked, and neither row is repointed or archived.
func TestCrossLibraryCopyStaysIndependent(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, adminPgID := moveTestAdminID(t, setup)
libA, folderA := createTypedTestLibrary(t, setup, "Copy Lib A", "ebooks")
libB, folderB := createTypedTestLibrary(t, setup, "Copy Lib B", "ebooks")
defer deleteLibraries(t, setup, libA, libB)
content := []byte("deliberate duplicate content")
writeTestEbook(t, folderA, content)
writeTestEbook(t, folderB, content)
scanTestFolder(t, setup, adminID, folderA)
scanTestFolder(t, setup, adminID, folderB)
var itemA, itemB database.ListMediaItemsByLibraryRow
waitForCond(t, 5*time.Second, "both libraries should hold their own copy", func() bool {
booksA, errA := setup.DB.ListMediaItemsByLibrary(ctx, libA)
booksB, errB := setup.DB.ListMediaItemsByLibrary(ctx, libB)
if errA != nil || errB != nil || len(booksA) != 1 || len(booksB) != 1 {
return false
}
itemA, itemB = booksA[0], booksB[0]
return true
})
assert.NotEqual(t, itemA.ID, itemB.ID, "deliberate copies must stay independent rows")
assert.False(t, itemA.ArchivedAt.Valid && itemB.ArchivedAt.Valid, "no copy should be archived")
// History on one copy does not leak into the other.
_, err := setup.DB.UpdateReadingProgress(ctx, database.UpdateReadingProgressParams{
MediaItemID: itemA.ID,
UserID: adminPgID,
CurrentPage: pgtype.Int4{Int32: 7, Valid: true},
TotalPages: pgtype.Int4{Int32: 100, Valid: true},
})
require.NoError(t, err)
_, err = setup.DB.GetReadingProgress(ctx, database.GetReadingProgressParams{MediaItemID: itemB.ID, UserID: adminPgID})
assert.Error(t, err, "copy B must not inherit copy A's progress")
}
// TestCrossLibraryMoveRejectedForTypeMismatch verifies the type guard: the
// cross-library lookup only matches same-type libraries, so a reflowable EPUB
// registered in a manga library whose file moves to ANOTHER manga library is
// detected as a move but must be rejected by the type rules (manga libraries
// only accept fixed-layout/image content). The move is rejected, a processing
// issue is recorded, and the file stays untracked rather than being
// force-imported.
func TestCrossLibraryMoveRejectedForTypeMismatch(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, _ := moveTestAdminID(t, setup)
libA, folderA := createTypedTestLibrary(t, setup, "Type Src Manga", "manga")
libM, folderM := createTypedTestLibrary(t, setup, "Type Dst Manga", "manga")
defer deleteLibraries(t, setup, libA, libM)
content := []byte("reflowable epub that manga libraries must not adopt via move")
writeTestEbook(t, folderA, content)
scanTestFolder(t, setup, adminID, folderA)
var original database.ListMediaItemsByLibraryRow
waitForCond(t, 5*time.Second, "book should be imported into the first manga library", func() bool {
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
if err != nil || len(books) != 1 {
return false
}
original = books[0]
return true
})
require.Equal(t, "reflowable", original.FormatGroup, "dummy epub must scan as reflowable for this test")
// Move into the second manga library: same hash, same type, but the
// reflowable format violates manga library rules.
writeTestEbook(t, folderM, content)
require.NoError(t, os.Remove(filepath.Join(folderA, "Author", moveTestFileName)))
scanTestFolder(t, setup, adminID, folderM)
// The second manga library must not adopt the item.
booksM, err := setup.DB.ListMediaItemsByLibrary(ctx, libM)
require.NoError(t, err)
assert.Empty(t, booksM, "reflowable epub must not be repointed between manga libraries")
// The original row is untouched in the first manga library.
after, err := setup.DB.GetMediaItem(ctx, original.ID)
require.NoError(t, err)
assert.Equal(t, libA, after.LibraryID, "original row must stay in its library")
// A processing issue explains the rejection.
waitForCond(t, 5*time.Second, "processing issue should be recorded", func() bool {
issues, err := setup.DB.ListProcessingIssuesByLibrary(ctx, libM)
if err != nil {
return false
}
for _, issue := range issues {
if issue.IssueType == "library_type_mismatch" {
return true
}
}
return false
})
}
// TestListHiddenMediaItemsMatchesActiveTwin verifies the archived-page query:
// an archived row with an active same-hash twin reports it in match_* columns;
// an archived row without a twin reports no match.
func TestListHiddenMediaItemsMatchesActiveTwin(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, _ := moveTestAdminID(t, setup)
libA, folderA := createTypedTestLibrary(t, setup, "Twin Lib A", "ebooks")
libB, folderB := createTypedTestLibrary(t, setup, "Twin Lib B", "ebooks")
libC, folderC := createTypedTestLibrary(t, setup, "Twin Lib C", "ebooks")
defer deleteLibraries(t, setup, libA, libB, libC)
twinContent := []byte("twin content shared by two libraries")
writeTestEbook(t, folderA, twinContent)
writeTestEbook(t, folderB, twinContent)
lonelyContent := []byte("unique content with no twin anywhere")
writeTestEbook(t, folderC, lonelyContent)
scanTestFolder(t, setup, adminID, folderA)
scanTestFolder(t, setup, adminID, folderB)
scanTestFolder(t, setup, adminID, folderC)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
twin := booksA[0]
booksC, err := setup.DB.ListMediaItemsByLibrary(ctx, libC)
require.NoError(t, err)
require.Len(t, booksC, 1)
lonely := booksC[0]
// Archive both: the A row now has an active twin in B, the C row has none.
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, twin.ID))
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, lonely.ID))
rows, err := setup.DB.ListHiddenMediaItems(ctx)
require.NoError(t, err)
foundTwin, foundLonely := false, false
for _, row := range rows {
if row.ID == twin.ID {
foundTwin = true
assert.True(t, row.MatchID.Valid, "twin row must report its active match")
if row.MatchID.Valid {
assert.Equal(t, findTwinTargetID(t, setup, libB), uuid.UUID(row.MatchID.Bytes).String())
}
assert.Equal(t, "Test Book", row.MatchTitle)
}
if row.ID == lonely.ID {
foundLonely = true
assert.False(t, row.MatchID.Valid, "row without a twin must report no match")
}
}
require.True(t, foundTwin, "twin row should be listed")
require.True(t, foundLonely, "lonely row should be listed")
}
// findTwinTargetID returns the single active item in the given library.
func findTwinTargetID(t *testing.T, setup *TestServerSetup, libID pgtype.UUID) string {
t.Helper()
books, err := setup.DB.ListMediaItemsByLibrary(context.Background(), libID)
require.NoError(t, err)
require.Len(t, books, 1)
return uuid.UUID(books[0].ID.Bytes).String()
}
// mergeRequest issues POST /api/media-items/{source}/merge.
func mergeRequest(t *testing.T, setup *TestServerSetup, token, sourceID string, body map[string]interface{}) *http.Response {
t.Helper()
payload, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items/"+sourceID+"/merge", bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
return resp
}
// archiveAndIsolate imports one book into the library and archives it,
// simulating a file that was copied elsewhere then deleted from here.
func archiveAndIsolate(t *testing.T, setup *TestServerSetup, adminID, adminPgID pgtype.UUID, libID pgtype.UUID, folder, content string) database.ListMediaItemsByLibraryRow {
t.Helper()
ctx := context.Background()
writeTestEbook(t, folder, []byte(content))
scanTestFolder(t, setup, adminID, folder)
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libID)
require.NoError(t, err)
require.Len(t, books, 1)
item := books[0]
// Reading history that must survive the merge.
_, err = setup.DB.UpdateReadingProgress(ctx, database.UpdateReadingProgressParams{
MediaItemID: item.ID,
UserID: adminPgID,
CurrentPage: pgtype.Int4{Int32: 42, Valid: true},
TotalPages: pgtype.Int4{Int32: 200, Valid: true},
})
require.NoError(t, err)
_, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
MediaItemID: item.ID,
UserID: adminPgID,
SelectionText: "annotation on the old copy",
Color: pgtype.Text{String: "#00ff00", Valid: true},
})
require.NoError(t, err)
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, item.ID))
return item
}
// TestMergeArchivedItemIntoActiveTwin covers the copy-then-delete-later
// workflow: the active copy exists in another library, the archived twin
// holds the real reading history. Merging moves progress + annotations onto
// the active row and removes the archived row.
func TestMergeArchivedItemIntoActiveTwin(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, adminPgID := moveTestAdminID(t, setup)
libOld, folderOld := createTypedTestLibrary(t, setup, "Merge Old Lib", "ebooks")
libNew, folderNew := createTypedTestLibrary(t, setup, "Merge New Lib", "ebooks")
defer deleteLibraries(t, setup, libOld, libNew)
content := "merge workflow epub content"
// Old library: book + history, then archived (file "deleted").
oldItem := archiveAndIsolate(t, setup, adminID, adminPgID, libOld, folderOld, content)
// New library: the surviving active copy, no history of its own.
writeTestEbook(t, folderNew, []byte(content))
scanTestFolder(t, setup, adminID, folderNew)
booksNew, err := setup.DB.ListMediaItemsByLibrary(ctx, libNew)
require.NoError(t, err)
require.Len(t, booksNew, 1)
active := booksNew[0]
oldID := uuid.UUID(oldItem.ID.Bytes).String()
activeID := uuid.UUID(active.ID.Bytes).String()
resp := mergeRequest(t, setup, setup.Token, oldID, map[string]interface{}{"target_id": activeID})
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusOK, resp.StatusCode, "merge should succeed")
var mergeResp map[string]interface{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&mergeResp))
// Archived row is gone.
_, err = setup.DB.GetMediaItem(ctx, oldItem.ID)
assert.Error(t, err, "archived row must be removed after merge")
// History landed on the active row.
progress, err := setup.DB.GetReadingProgress(ctx, database.GetReadingProgressParams{MediaItemID: active.ID, UserID: adminPgID})
require.NoError(t, err)
assert.Equal(t, int32(42), progress.CurrentPage.Int32, "progress must merge into the active copy")
highlights, err := setup.DB.GetMediaHighlights(ctx, database.GetMediaHighlightsParams{MediaItemID: active.ID, UserID: adminPgID})
require.NoError(t, err)
assert.Len(t, highlights, 1, "annotations must merge into the active copy")
booksOld, err := setup.DB.ListMediaItemsByLibrary(ctx, libOld)
require.NoError(t, err)
assert.Empty(t, booksOld, "old library must be empty after merge")
}
func TestMergeArchivedItemRejections(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, adminPgID := moveTestAdminID(t, setup)
libA, folderA := createTypedTestLibrary(t, setup, "Reject Lib A", "ebooks")
libB, folderB := createTypedTestLibrary(t, setup, "Reject Lib B", "ebooks")
defer deleteLibraries(t, setup, libA, libB)
t.Run("requires admin", func(t *testing.T) {
writeTestEbook(t, folderA, []byte("admin check content"))
scanTestFolder(t, setup, adminID, folderA)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
itemID := uuid.UUID(booksA[0].ID.Bytes).String()
resp := mergeRequest(t, setup, setup.RegularToken, itemID, map[string]interface{}{"target_id": itemID})
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("rejects active source", func(t *testing.T) {
writeTestEbook(t, folderA, []byte("active source content"))
scanTestFolder(t, setup, adminID, folderA)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
writeTestEbook(t, folderB, []byte("active source content"))
scanTestFolder(t, setup, adminID, folderB)
targetID := findTwinTargetID(t, setup, libB)
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": targetID})
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "source is not archived/missing")
})
t.Run("rejects hash mismatch", func(t *testing.T) {
writeTestEbook(t, folderA, []byte("hash mismatch content A"))
scanTestFolder(t, setup, adminID, folderA)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
source := booksA[0]
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, source.ID))
sourceID := uuid.UUID(source.ID.Bytes).String()
writeTestEbook(t, folderB, []byte("hash mismatch content B - different bytes"))
scanTestFolder(t, setup, adminID, folderB)
targetID := findTwinTargetID(t, setup, libB)
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": targetID})
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "different content must not merge")
// Both rows still exist.
_, err = setup.DB.GetMediaItem(ctx, source.ID)
assert.NoError(t, err, "source must survive a rejected merge")
})
t.Run("requires target_id", func(t *testing.T) {
writeTestEbook(t, folderA, []byte("missing target content"))
scanTestFolder(t, setup, adminID, folderA)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksA[0].ID))
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{})
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("rejects self merge", func(t *testing.T) {
writeTestEbook(t, folderA, []byte("self merge content"))
scanTestFolder(t, setup, adminID, folderA)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksA[0].ID))
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": sourceID})
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("rejects archived target", func(t *testing.T) {
content := []byte("archived target content")
writeTestEbook(t, folderA, content)
scanTestFolder(t, setup, adminID, folderA)
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
require.NoError(t, err)
require.Len(t, booksA, 1)
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksA[0].ID))
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
writeTestEbook(t, folderB, content)
scanTestFolder(t, setup, adminID, folderB)
booksB, err := setup.DB.ListMediaItemsByLibrary(ctx, libB)
require.NoError(t, err)
require.Len(t, booksB, 1)
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksB[0].ID))
targetID := uuid.UUID(booksB[0].ID.Bytes).String()
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": targetID})
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "target must be active")
})
_ = adminPgID
}
// TestLibraryTypeNameTriggerOnLibraryChange proves the schema trigger keeps
// media_items.library_type_name in sync when a row is repointed to a library
// of a different type (the INSERT-only trigger previously left it stale).
func TestLibraryTypeNameTriggerOnLibraryChange(t *testing.T) {
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
ctx := context.Background()
adminID, _ := moveTestAdminID(t, setup)
libE, folderE := createTypedTestLibrary(t, setup, "Trigger Ebooks", "ebooks")
libC, _ := createTypedTestLibrary(t, setup, "Trigger Comics", "comics")
defer deleteLibraries(t, setup, libE, libC)
writeTestEbook(t, folderE, []byte("trigger check content"))
scanTestFolder(t, setup, adminID, folderE)
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libE)
require.NoError(t, err)
require.Len(t, books, 1)
item := books[0]
require.True(t, item.LibraryTypeName.Valid && item.LibraryTypeName.String == "ebooks")
require.NoError(t, setup.DB.MoveMediaItemToLibrary(ctx, database.MoveMediaItemToLibraryParams{
ID: item.ID,
LibraryID: libC,
FilePath: "Author/Test Book.epub",
FileSize: pgtype.Int8{Int64: 27, Valid: true},
}))
after, err := setup.DB.GetMediaItem(ctx, item.ID)
require.NoError(t, err)
assert.Equal(t, libC, after.LibraryID)
assert.True(t, after.LibraryTypeName.Valid && after.LibraryTypeName.String == "comics",
"trigger must refresh library_type_name when library_id changes")
}
+82
View File
@@ -252,6 +252,88 @@ func TestListPendingRegistrations(t *testing.T) {
assert.NotNil(t, pending, "Pending registrations should not be nil")
}
// Regression test for the "approve returns 500 after app reinstall" bug:
// an app reinstall that preserves data (e.g. Android Studio installDebug
// over an existing install) re-registers with the SAME device_identifier,
// and the blind INSERT in ApproveDevice hit the UNIQUE(device_identifier)
// constraint. Re-approval must be idempotent: same row (id unchanged),
// rotated token, old token invalidated.
func TestApproveDeviceReapprovalRotatesToken(t *testing.T) {
setup := setupTestServer(t)
userID := getTestUserID(t, setup.DB)
identifier := fmt.Sprintf("repro-reinstall-%s", uuid.New().String())
registerAndApprove := func() string {
regRequest := map[string]interface{}{
"device_name": "Reinstall Device",
"device_type": "mobile",
"device_identifier": identifier,
}
regBody, _ := json.Marshal(regRequest)
req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusCreated, rec.Code, "Should initiate registration")
var regResponse map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &regResponse)
registrationID, ok := regResponse["registration_id"].(string)
require.True(t, ok, "Should have registration_id")
req = httptest.NewRequest("GET", fmt.Sprintf("/api/devices/approve/%s", registrationID), nil)
req.Header.Set("Authorization", "Bearer "+setup.Token)
rec = httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code,
"Approve must succeed even when the identifier already exists (was the reinstall 500)")
// The status response is single-use and carries the credentials.
statusBody, _ := json.Marshal(map[string]string{"registration_id": registrationID})
req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody))
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var statusResponse map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &statusResponse)
require.Equal(t, "approved", statusResponse["status"])
token, ok := statusResponse["auth_token"].(string)
require.True(t, ok, "Should have auth_token")
require.NotEmpty(t, token)
return token
}
token1 := registerAndApprove()
token2 := registerAndApprove()
assert.NotEqual(t, token1, token2, "Re-approval must rotate the auth token (fresh install = fresh credentials)")
devices, err := setup.DB.ListDevicesByUser(context.Background(),
pgtype.UUID{Bytes: [16]byte(userID), Valid: true})
require.NoError(t, err)
rows := 0
for _, d := range devices {
if d.DeviceIdentifier == identifier {
rows++
}
}
assert.Equal(t, 1, rows, "Re-approval must reuse the existing devices row, not duplicate it")
// The rotated-in token must be the live one.
var live *database.Devices
for i := range devices {
if devices[i].DeviceIdentifier == identifier {
live = &devices[i]
}
}
require.NotNil(t, live)
assert.Equal(t, token2, live.AuthToken, "The devices row must carry the newly rotated token")
}
func TestApproveDeviceRegistration(t *testing.T) {
setup := setupTestServer(t)
+113
View File
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -435,3 +436,115 @@ func TestOPDSSearchAcrossLibraries(t *testing.T) {
t.Logf("❌ FAILED - OPDS returned %d", resp.StatusCode)
}
}
// TestOPDSLibraryFolders covers the library-folder navigation feed: the root
// catalog is a navigation feed with an "All Books" entry plus one folder per
// visible library, the scoped catalog only lists that library's books, and
// the library-scoped search parameter restricts results to that library.
// TestOPDSLibraryFolders covers the library-folder navigation feed: the root
// catalog is a navigation feed with an "All Books" entry plus one folder per
// visible library, the scoped catalog only lists that library's books, and
// the library-scoped search parameter restricts results to that library.
// NOTE: library names deliberately avoid the word "test" — setupTestServer's
// setup-time cleanup deletes every library whose name contains it (and
// setupDeviceTest re-runs that cleanup mid-test).
func TestOPDSLibraryFolders(t *testing.T) {
setup := setupTestServer(t)
client := &http.Client{}
// Two libraries, one distinct book each.
lib1Resp := createLibrary(t, client, setup, "OPDS Folder Lib Alpha")
lib2Resp := createLibrary(t, client, setup, "OPDS Folder Lib Beta")
lib1ID := lib1Resp["id"].(string)
lib2ID := lib2Resp["id"].(string)
// Real directories: the folder API validates that the path exists on the
// host running the server (container path /app/uploads won't exist when
// tests run on the host).
addFolderToLibrary(t, setup, lib1ID, t.TempDir())
addFolderToLibrary(t, setup, lib2ID, t.TempDir())
createTestMediaItemIDInLibrary(t, client, setup, lib1ID, "OPDS Folder Book 1")
createTestMediaItemIDInLibrary(t, client, setup, lib2ID, "OPDS Folder Book 2")
deviceSetup := setupDeviceTest(t)
device := deviceSetup.CreateDevice(t, "OPDS Folders Device", "koreader", "opds-folders-"+uuid.NewString())
base := setup.Server.URL + "/opds/devices/" + device.ID.String()
// Explicit cleanup: these names don't contain "test", so the harness
// cleanup skips them; remove them ourselves. Registered as a defer AFTER
// the setup.Close defer so LIFO ordering runs it while the pool is open.
defer deleteLibraries(t, setup,
pgtype.UUID{Bytes: uuid.MustParse(lib1ID), Valid: true},
pgtype.UUID{Bytes: uuid.MustParse(lib2ID), Valid: true})
getFeed := func(t *testing.T, url string) (int, string, string) {
t.Helper()
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+device.AuthToken)
resp, err := client.Do(req)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
contentType := resp.Header.Get("Content-Type")
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
return resp.StatusCode, contentType, string(bodyBytes)
}
t.Run("root catalog is a navigation feed with All Books and library folders", func(t *testing.T) {
status, contentType, body := getFeed(t, base+"/catalog")
require.Equal(t, http.StatusOK, status)
assert.Contains(t, contentType, "kind=navigation")
assert.Contains(t, body, "All Books")
assert.Contains(t, body, "OPDS Folder Lib Alpha")
assert.Contains(t, body, "OPDS Folder Lib Beta")
assert.Contains(t, body, "/library/"+lib1ID+"/catalog")
assert.Contains(t, body, "/library/"+lib2ID+"/catalog")
// The nav feed itself must not inline book entries.
assert.NotContains(t, body, "OPDS Folder Book 1")
})
t.Run("all=1 keeps the flat cross-library catalog", func(t *testing.T) {
status, contentType, body := getFeed(t, base+"/catalog?all=1")
require.Equal(t, http.StatusOK, status)
assert.Contains(t, contentType, "kind=acquisition")
assert.Contains(t, body, "OPDS Folder Book 1")
assert.Contains(t, body, "OPDS Folder Book 2")
})
t.Run("scoped catalog lists only its own library's books", func(t *testing.T) {
status, contentType, body := getFeed(t, base+"/library/"+lib1ID+"/catalog")
require.Equal(t, http.StatusOK, status)
assert.Contains(t, contentType, "kind=acquisition")
assert.Contains(t, body, "OPDS Folder Book 1")
assert.NotContains(t, body, "OPDS Folder Book 2")
// Scoped search discovery: template pins the library.
assert.Contains(t, body, "library_id="+lib1ID)
})
t.Run("scoped catalog rejects libraries outside device visibility", func(t *testing.T) {
status, _, _ := getFeed(t, base+"/library/11111111-2222-3333-4444-555555555555/catalog")
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("scoped search restricts results to the library", func(t *testing.T) {
status, _, body := getFeed(t, base+"/search?q=OPDS+Folder&library_id="+lib2ID)
require.Equal(t, http.StatusOK, status)
assert.Contains(t, body, "OPDS Folder Book 2")
assert.NotContains(t, body, "OPDS Folder Book 1")
})
t.Run("scoped search rejects inaccessible library", func(t *testing.T) {
status, _, _ := getFeed(t, base+"/search?q=OPDS&library_id=11111111-2222-3333-4444-555555555555")
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("global search labels entries with their library", func(t *testing.T) {
status, _, body := getFeed(t, base+"/search?q=OPDS+Folder")
require.Equal(t, http.StatusOK, status)
assert.Contains(t, body, "OPDS Folder Book 1")
assert.Contains(t, body, "OPDS Folder Book 2")
assert.Contains(t, body, "OPDS Folder Lib Alpha")
assert.Contains(t, body, "OPDS Folder Lib Beta")
})
}
@@ -0,0 +1,53 @@
package main
import (
"context"
"testing"
"bookhoard/internal/config"
"bookhoard/internal/database"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require"
)
// TestSchemaInitializationIsIdempotent replays the application's production
// schema initializer twice against the same database. schema.sql runs on
// EVERY app startup, so it must apply cleanly to a database where a previous
// startup already applied it. A regression here crash-loops the production
// container on its second boot (e.g. SQLSTATE 2BP01: DROP FUNCTION refused
// while a trigger created by the previous run still references it).
//
// Uses a dedicated pool: production Initialize holds the advisory lock on one
// connection while executing the schema on another, but the shared test pool
// is capped at MaxConns=1 and would deadlock.
func TestSchemaInitializationIsIdempotent(t *testing.T) {
if testing.Short() {
t.Skip("skipping schema re-initialization in -short mode")
}
// Bring up the standard test environment (seeds users, starts server).
setup := setupTestServer(t)
defer func() { _ = setup.Close() }()
// Dedicated pool with default connection limits for the initializer.
poolCfg, err := pgxpool.ParseConfig(config.LoadConfig().DatabaseURL())
require.NoError(t, err, "failed to parse database URL")
pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
require.NoError(t, err, "failed to connect for schema initialization test")
defer func() { pool.Close() }()
ctx := context.Background()
// First boot: applies the schema to the (possibly fresh) database.
if err := database.Initialize(ctx, pool); err != nil {
t.Fatalf("first schema initialization failed: %v", err)
}
// Second boot against the now-initialized database: this is exactly
// where non-idempotent statements (bare CREATE TRIGGER, DROP FUNCTION
// with dependent objects, etc.) blow up.
if err := database.Initialize(ctx, pool); err != nil {
t.Fatalf("second schema initialization failed: %v — schema.sql must stay idempotent across restarts", err)
}
}
+38 -14
View File
@@ -544,6 +544,10 @@ func setupTestServer(t *testing.T) *TestServerSetup {
AnnotationService: annotationService,
DeviceAuthMiddleware: deviceAuthMiddleware,
LoginTracker: loginAttemptTracker,
// RegisterRoutes dereferences Settings for the auth rate limit;
// the main binary wires database.NewSettingsRegistry(queries) —
// the test harness must too or route registration nil-panics.
Settings: database.NewSettingsRegistry(queries),
}
router.RegisterRoutes(routerConfig)
@@ -588,21 +592,41 @@ func setupTestServer(t *testing.T) *TestServerSetup {
}
}
// Create fresh admin test user
// The admin test user doubles as the shared dev admin and is
// deliberately PRESERVED by the cleanup above — reuse it when it
// already exists instead of re-creating (a blind INSERT hits
// users_email_key on every run after the first).
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
adminUser, err := queries.CreateUser(ctx, database.CreateUserParams{
Email: "testuser@tests.bookhoard.internal",
Username: "testuser",
PasswordHash: passwordHash,
FirstName: pgtype.Text{String: "Test", Valid: true},
LastName: pgtype.Text{String: "User", Valid: true},
Role: "admin",
})
require.NoError(t, err, "Failed to create admin test user")
adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:])
require.NoError(t, err, "Failed to parse admin user UUID")
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
var adminUUID uuid.UUID
if existing, gerr := queries.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal"); gerr == nil {
adminUUID, err = uuid.FromBytes(existing.ID.Bytes[:])
require.NoError(t, err, "Failed to parse admin user UUID")
// Reset the password to the known test constant: other tests (e.g.
// password-change tests) mutate the shared admin's password, which
// would otherwise lock every later test out with login 401s until
// the database is wiped.
if updErr := queries.UpdatePassword(ctx, database.UpdatePasswordParams{
ID: existing.ID,
PasswordHash: passwordHash,
}); updErr != nil {
require.NoError(t, updErr, "Failed to reset shared admin test password")
}
} else {
adminUser, cerr := queries.CreateUser(ctx, database.CreateUserParams{
Email: "testuser@tests.bookhoard.internal",
Username: "testuser",
PasswordHash: passwordHash,
FirstName: pgtype.Text{String: "Test", Valid: true},
LastName: pgtype.Text{String: "User", Valid: true},
Role: "admin",
})
require.NoError(t, cerr, "Failed to create admin test user")
adminUUID, err = uuid.FromBytes(adminUser.ID.Bytes[:])
require.NoError(t, err, "Failed to parse admin user UUID")
// Default collections are created exactly once per user; the
// preserved dev admin already has its set from the first run.
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
}
// Create fresh regular test user
regularUser, err := queries.CreateUser(ctx, database.CreateUserParams{
+12 -1
View File
@@ -259,8 +259,11 @@ CREATE INDEX IF NOT EXISTS idx_media_items_archived ON media_items (archived_at)
CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN (tags_search);
CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search);
-- Drop existing trigger if exists
-- Drop existing triggers if exists. Both must be dropped BEFORE the function:
-- dropping a function that triggers still reference fails (SQLSTATE 2BP01),
-- which would abort the schema transaction on every startup after the first.
DROP TRIGGER IF EXISTS set_library_type_name_on_insert ON media_items;
DROP TRIGGER IF EXISTS set_library_type_name_on_library_change ON media_items;
-- Drop existing function if exists
DROP FUNCTION IF EXISTS set_library_type_name();
@@ -279,6 +282,14 @@ CREATE TRIGGER set_library_type_name_on_insert
FOR EACH ROW
EXECUTE FUNCTION set_library_type_name();
-- Keep library_type_name truthful when an item is repointed to another
-- library (cross-library move detection reuses the same function; the row
-- keeps its ID while its library changes).
CREATE TRIGGER set_library_type_name_on_library_change
BEFORE UPDATE OF library_id ON media_items
FOR EACH ROW
EXECUTE FUNCTION set_library_type_name();
-- Add GIN indexes for pg_trgm fuzzy search performance
CREATE INDEX IF NOT EXISTS idx_media_items_author_trgm
ON media_items USING GIN (author gin_trgm_ops);
+40 -108
View File
@@ -394,6 +394,11 @@ Authorization: Bearer <token>
## Reading Progress
Full field reference: [api/progress/](api/progress/) — and read the
[Position Contract](api/progress/position-contract.md) (verification,
healing, the OPF spine numbering hazard, offset currencies) before
writing a client.
### Get Reading Progress
```http
@@ -405,57 +410,56 @@ Authorization: Bearer <token>
```json
{
"id": "uuid",
"media_item_id": "uuid",
"user_id": "uuid",
"current_page": 45,
"total_pages": 200,
"percentage": 0.225,
"character_offset": 15432,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3,
"chapter_progress": 0.5,
"last_read_at": "2026-01-31T10:00:00Z",
"percentage": 0.045,
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)",
"context_text": "from day to day, but there was none in which ...",
"character_offset": 16375,
"current_page": null,
"total_pages": null,
"chapter": null,
"chapter_progress": null,
"format_group": "reflowable",
"viewport_y": 0.12,
"zoom_level": 1.0
"total_characters": 592216,
"chapter_count": 1,
"last_read_at": "2026-09-26T14:52:34Z",
"last_sync_source": "koreader",
"css_selector": "body>div:nth-child(4)>p:nth-child(29)",
"anchor_href": "1984.xhtml",
"char_offset": 598
}
```
`css_selector`, `anchor_href` and `char_offset` are server-derived
restore handles, served for convertible reflowable books with a
resolvable anchor. `char_offset` is UTF-16 code units within the anchor
block's text; `character_offset` is a book-wide rune count. Resolve the
document by `anchor_href`, never by the CFI's spine step (OPF numbering
includes `linear="no"` items).
### Update Reading Progress
```http
PUT /api/media-items/{media_id}/progress
Authorization: Bearer <token>
Content-Type: application/json
{
"source": "web",
"location": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"page": 89,
"total_pages": 200
},
"device_metadata": {
"device_type": "web",
"user_agent": "Mozilla/5.0..."
}
}
```
**Response** (200):
```json
{
"sync_status": "success",
"progress_updated": true,
"devices_notified": ["device-1", "device-2"],
"broadcast": true
"percentage": 0.0415,
"context_text": "was at war with one of these Powers it was generally a",
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)"
}
```
All fields are optional. The server verifies the submission against the
book and heals it on mismatch; the response is the stored row after
verification. A `percentage` below 0.005 is ignored while stored
progress exceeds 0.01 (first-page anti-clobber).
### Delete Reading Progress
```http
@@ -1297,82 +1301,10 @@ Authorization: Bearer <device_token>
## Universal Progress
### Get Universal Progress
```http
GET /api/progress/{book_uuid}
Authorization: Bearer <token>
```
**Response** (200):
```json
{
"book_id": "book-uuid",
"format_group": "reflowable",
"universal_progress": 0.45678,
"location_references": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"chapter_progress": 0.234,
"viewport_y": 0.12
},
"device_progress": {
"koreader": {
"percentage": 0.45678,
"last_sync": "2026-01-30T20:00:00Z"
},
"kobo": {
"percentage": 45.6,
"last_sync": "2026-01-30T19:55:00Z"
},
"web": {
"display_page": 89,
"total_pages": 200,
"last_sync": "2026-01-30T20:05:00Z"
}
},
"annotations": {
"highlights": [...],
"notes": [...],
"bookmarks": [...]
},
"conflicts": [
{
"id": "conflict-uuid",
"type": "progress",
"resolved": false,
"sources": ["koreader", "kobo"]
}
]
}
```
### Update Universal Progress
```http
POST /api/progress/{book_uuid}
Authorization: Bearer <token>
Content-Type: application/json
{
"source": "web|koreader|kobo|mobile",
"location": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"page": 89,
"total_pages": 200
},
"device_metadata": {
"device_type": "web",
"user_agent": "..."
}
}
```
Universal progress is the media-item progress — one row per (user,
media item) shared by every device. See [Reading Progress](#reading-progress)
and the [Position Contract](api/progress/position-contract.md). There
are no separate `/api/progress/:id` GET/POST endpoints.
## Conflicts
+7 -3
View File
@@ -117,10 +117,14 @@ See [Media Item Operations](media-items/)
## Reading Progress
See [Progress Tracking](progress/)
See [Progress Tracking](progress/) — in particular the
[Position Contract](progress/position-contract.md) (verification,
healing, the OPF spine numbering hazard, and offset currencies) before
writing a client.
- GET /api/progress/:id - Get universal progress
- POST /api/progress/:id - Update universal progress
- GET /api/media-items/:id/progress - Get reading progress + restore handles
- PUT /api/media-items/:id/progress - Submit reading progress (verified/healed server-side)
- DELETE /api/media-items/:id/progress - Delete reading progress
- GET /api/progress/:id/history - Get progress history
## Notes & Highlights
+42 -25
View File
@@ -1,9 +1,12 @@
# Get Metadata
Get metadata for a book from KOReader device.
Get a book's stored progress and annotations for a KOReader device —
the pull half of the device sync. The reference client calls this after
linking a book via [Resolve Book](resolve_book.md) and navigates to the
returned position.
**Endpoint**: `GET /api/sync/koreader/metadata/:uuid`
**Auth**: Required (Device authentication)
**Auth**: Required (Device authentication — `Authorization: Bearer {device_token}`)
## Path Parameters
@@ -11,41 +14,55 @@ Get metadata for a book from KOReader device.
| --------- | ------------- | -------- | ----------- |
| uuid | string (UUID) | Yes | Book UUID |
## Device Authentication
This endpoint requires device authentication (not user JWT). Devices authenticate using their device credentials.
## Request Headers
| Header | Type | Required | Description |
| ------------ | ------ | -------- | ------------------------- |
| X-Device-ID | string | Yes | Device UUID |
| X-Device-Key | string | Yes | Device authentication key |
### Example Request
```http
GET /api/sync/koreader/metadata/550e8400-e29b-41d4-a716-446655440000
X-Device-ID: 550e8400-e29b-41d4-a716-446655440000
X-Device-Key: device-auth-key
GET /api/sync/koreader/metadata/774641f9-317b-4087-8e04-53bb4392ae56
Authorization: Bearer {device_token}
```
## Response (200 OK)
```json
{
"id": "uuid",
"title": "Book Title",
"authors": ["Author Name"],
"path": "/path/to/book.epub",
"file_size": 1234567,
"modified_at": "2026-02-08T10:00:00Z"
"uuid": "774641f9-317b-4087-8e04-53bb4392ae56",
"title": "1984",
"author": "George Orwell",
"progress": {
"percentage": 0.045,
"koreader_xpointer": "/body/DocFragment[1]/body/p[29]/text().598",
"chapter": null,
"chapter_progress": null,
"page": null,
"total_pages": null
},
"annotations": {
"highlights": []
}
}
```
### Progress Object
| Field | Type | Description |
| ----- | ---- | ----------- |
| `percentage` | float | Stored position as a book fraction. |
| `koreader_xpointer` | string | The stored canonical position converted back to a CRE xpointer (UTF-16 `text().N` offset). **The device should navigate to this.** Reflowable books only. |
| `epubcfi` | string | The stored canonical CFI, when the conversion to a CRE xpointer is unavailable. Fallback after `koreader_xpointer`. |
| `character` | int | Book-wide rune offset (internal currency). |
| `chapter`, `chapter_progress` | int, float | Chapter position when known. |
| `page`, `total_pages` | int | Fixed-layout page position — the canonical locator for image-based books (CFI/xpointer are omitted for them). |
`progress` is `null` when the book has no stored progress.
The `annotations` object carries device-format highlights/bookmarks/notes
synced from other clients; its presence depends on annotation sync being
enabled.
## Error Responses
| Code | Description |
| ---- | ---------------------------- |
| Code | Description |
| ---- | ----------- |
| 400 | Invalid book UUID |
| 401 | Device authentication failed |
| 404 | Book or device not found |
| 404 | Book not found |
+58 -34
View File
@@ -1,63 +1,87 @@
# Sync Progress
Sync reading progress from KOReader device.
Push reading progress from a KOReader device.
**Endpoint**: `POST /api/sync/koreader/progress`
**Auth**: Required (Device authentication)
**Auth**: Required (Device authentication — `Authorization: Bearer {device_token}`)
## Device Authentication
This endpoint requires device authentication (not user JWT). Devices authenticate using their device credentials.
This is the device-native tier of the [Position
Contract](../progress/position-contract.md): the KOReader payload carries
a CRE xpointer and the server converts it to the canonical standard CFI,
verifies it against the submitted `context_text`, and heals it on
mismatch — exactly like every other client.
## Request Body
| Field | Type | Required | Description |
| --------- | ------------- | -------- | ------------------------- |
| device_id | string (UUID) | Yes | Device UUID |
| progress | array | Yes | Array of progress objects |
| Field | Type | Required | Description |
| --------- | ------ | -------- | ----------- |
| `books` | array | Yes | One book object (the reference client sends a single-element array). |
| `sync_mode`| string | No | `immediate` (default) or `manual`. |
### Progress Object
### Book Object
| Field | Type | Required | Description |
| ----------- | ------- | -------- | ---------------------------------- |
| book | string | Yes | Book identifier (filename or UUID) |
| percent | float | Yes | Progress percentage (0-100) |
| page | integer | No | Current page number |
| total_pages | integer | No | Total pages in document |
| date_read | string | No | ISO 8601 timestamp of last read |
| updated_at | string | Yes | ISO 8601 timestamp |
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `uuid` | string (UUID) | No | Bookhoard UUID, once the device has linked the book via [Resolve Book](resolve_book.md). |
| `sha256` | string | Yes | File content hash (64 hex chars) — the primary book identity. |
| `title` | string | No | Document title. |
| `authors` | array | No | Author names. |
| `percentage` | float | Yes | Position as a fraction of the book (0..1). |
| `context_text` | string | No | Up to 100 whitespace-normalized chars from the current position — enables the server's verification/healing. Strongly recommended. |
| `page` | int | No | Current page (fixed-layout books). |
| `total_pages` | int | No | Page count (fixed-layout books). |
| `epubcfi` | string | Reflowable only | **A CRE xpointer** (`/body/DocFragment[N]/body/...`), not a CFI — the field name is historical. Fixed-layout books must omit it and carry their position in `page`/`total_pages`. |
| `file_path` | string | No | Device-local file path (informational). |
| `device_info` | object | No | `{ koreader_version, device_model }`. |
### Example Request
```http
POST /api/sync/koreader/progress
Authorization: Bearer {device_token}
Content-Type: application/json
```
```json
{
"device_id": "550e8400-e29b-41d4-a716-446655440000",
"progress": [
"books": [
{
"book": "book.epub",
"percent": 75.5,
"page": 150,
"total_pages": 200,
"date_read": "2026-02-08T10:00:00Z",
"updated_at": "2026-02-08T10:00:00Z"
"uuid": "774641f9-317b-4087-8e04-53bb4392ae56",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"title": "1984",
"authors": ["George Orwell"],
"percentage": 0.045,
"context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim",
"page": 14,
"total_pages": 311,
"epubcfi": "/body/DocFragment[1]/body/p[29]/text().598",
"device_info": {
"koreader_version": "v2026.07.1",
"device_model": "emulator"
}
}
]
],
"sync_mode": "immediate"
}
```
## Response (200 OK)
## Response (202 Accepted)
```json
{
"message": "Progress synced successfully",
"synced_count": 1
"sync_status": "ok",
"books_synced": 1,
"timestamp": "2026-09-26T21:25:09Z"
}
```
Per-book results and any detected sync conflicts are carried in
`book_results` and `conflicts` when present.
## Error Responses
| Code | Description |
| ---- | ---------------------------- |
| Code | Description |
| ---- | ----------- |
| 400 | Invalid request format |
| 401 | Device authentication failed |
| 400 | Invalid request data |
| 404 | Device not found |
| 500 | Database error |
@@ -0,0 +1,35 @@
# Delete Media Item Progress
Delete the stored reading progress for a media item.
**Endpoint**: `DELETE /api/media-items/:id/progress`
**Auth**: Required (Bearer token)
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | ---------------- |
| id | string (UUID) | Yes | Media item UUID |
### Example Request
```http
DELETE /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
## Response (200 OK)
```json
{
"message": "reading progress deleted"
}
```
## Error Responses
| Code | Description |
| ---- | ----------- |
| 400 | Invalid media item id |
| 401 | Invalid or expired token |
| 500 | Database error |
@@ -1,36 +0,0 @@
# Delete Reading Progress
Delete reading progress for a media item.
**Endpoint**: `DELETE /api/media-items/{media_id}/progress`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
| ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token |
### Example Request
```http
DELETE /api/media-items/uuid/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (204 No Content)
Progress deleted successfully.
## Error Responses
| Code | Description |
| ---- | ------------------------ |
| 401 | Invalid or expired token |
| 404 | Media item not found |
@@ -0,0 +1,98 @@
# Get Media Item Progress
Get the stored reading progress for a media item, plus the
server-derived restore handles for reflowable books.
**Endpoint**: `GET /api/media-items/:id/progress`
**Auth**: Required (Bearer token)
See [Position Contract](position-contract.md) for the semantics of every
field — what is verified, what the currencies are, and how clients
should restore.
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Media item UUID |
### Example Request
```http
GET /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
## Response (200 OK) — no progress stored
When the item has no progress row, an empty fixed-layout-style stub is
returned (not 404):
```json
{
"current_page": 0,
"total_pages": null
}
```
## Response (200 OK) — progress stored
```json
{
"id": "e8239659-0ed6-42ae-a946-8440d7655b42",
"media_item_id": "774641f9-317b-4087-8e04-53bb4392ae56",
"user_id": "1b64992e-3408-4d84-9e03-e2dc4950e1dd",
"current_page": null,
"total_pages": null,
"last_read_at": "2026-09-26T14:52:34.806446Z",
"percentage": 0.045,
"character_offset": 16375,
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)",
"chapter": null,
"chapter_progress": null,
"format_group": "reflowable",
"total_characters": 592216,
"chapter_count": 1,
"last_sync_device": "web",
"last_sync_source": "koreader",
"last_sync_timestamp": "2026-09-26T14:52:34.806446Z",
"css_selector": "body>div:nth-child(4)>p:nth-child(29)",
"anchor_href": "1984.xhtml",
"char_offset": 598,
"context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim"
}
```
### Field Reference
Base fields (always present when a row exists):
| Field | Type | Description |
| ----- | ---- | ----------- |
| `percentage` | float | Position as a fraction of the whole book (0..1). |
| `epubcfi` | string | The stored canonical standard CFI. **Spine steps index the OPF spine as written, including `linear="no"` items — do not resolve them against a readium reading order.** See the [Position Contract](position-contract.md#spine-numbering-hazard--read-this-before-parsing-a-stored-cfi). |
| `context_text` | string | The stored verification context (≤100 whitespace-normalized chars from the anchor). |
| `character_offset` | int | Book-wide rune offset. Internal currency — consistent with `total_characters`. |
| `current_page`, `total_pages` | int | Fixed-layout page position (null for reflowable). |
| `chapter`, `chapter_progress` | int, float | Chapter index and within-chapter fraction, when known. |
| `format_group` | string | `reflowable`, `fixed_layout`, `comic_archive`, … Gates the restore handles. |
| `total_characters`, `chapter_count` | int | Book metrics, for client-side fraction math. |
| `last_sync_device`, `last_sync_source`, `last_sync_timestamp` | — | Which client last wrote the row. |
Restore handles (conditional — served only for convertible reflowable
books whose stored anchor re-resolves at GET time):
| Field | Type | Description |
| ----- | ---- | ----------- |
| `anchor_href` | string | The spine document containing the anchor (e.g. `1984.xhtml`). **Resolve your resource by this**, never by the CFI's spine step. |
| `css_selector` | string | Body-relative chain to the anchor's block element. |
| `char_offset` | int | Anchor offset within the block's concatenated text, in UTF-16 code units. |
| `epubcfi` | string | Re-served (possibly healed) canonical CFI — present whenever re-verification produced one. |
## Error Responses
| Code | Description |
| ---- | ----------- |
| 400 | Invalid media item id |
| 401 | Invalid or expired token |
| 500 | Database error |
@@ -1,52 +0,0 @@
# Get Reading Progress
Retrieve reading progress for a specific media item.
**Endpoint**: `GET /api/media-items/{media_id}/progress`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
| ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/media-items/uuid/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"media_item_id": "uuid",
"user_id": "uuid",
"current_page": 45,
"total_pages": 200,
"percentage": 0.225,
"character_offset": 15432,
"epubcfi": "epubcfi(/6/4/2:15)",
"chapter": 3,
"chapter_progress": 0.5,
"last_read_at": "2026-01-31T10:00:00Z",
"format_group": "reflowable",
"viewport_y": 0.12,
"zoom_level": 1.0
}
```
## Error Responses
| Code | Description |
| ---- | ------------------------ |
| 401 | Invalid or expired token |
| 404 | Media item not found |
@@ -1,50 +0,0 @@
# Get Universal Progress
Get universal (device-agnostic) reading progress for a media item.
**Endpoint**: `GET /api/progress/:id`
**Auth**: Required
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Media item UUID |
## Request Headers
| Header | Type | Required | Description |
| ------------- | ------ | -------- | ------------ |
| Authorization | string | Yes | Bearer token |
### Example Request
```http
GET /api/progress/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## Response (200 OK)
```json
{
"media_item_id": "uuid",
"percentage": 75.5,
"position": 1234,
"page": 150,
"total_pages": 200,
"finished": false,
"updated_at": "2026-02-08T10:00:00Z",
"device": {
"id": "device-uuid",
"name": "My Kobo"
}
}
```
## Error Responses
| Code | Description |
| ---- | ------------------------ |
| 401 | Invalid or expired token |
| 404 | Media item not found |
@@ -0,0 +1,135 @@
# Position Contract
How reading positions are represented, submitted, verified, and restored
across clients. This is the contract every client speaks — readium-based
apps, web (foliate), KOReader, and Kobo devices alike.
**The server is the position authority.** Every submission is
independently verified against the book itself before it is stored; a
client whose locator math is wrong cannot poison the stored position.
In exchange, the server hands back handles clients can apply directly,
so they never have to parse or trust CFIs themselves.
## The three-tier submission
Clients submit what their renderer can reliably observe. All three tiers
are accepted on `PUT /api/media-items/:id/progress` (and on the KOReader
device endpoints):
| Tier | Field | Currency | Notes |
| ------ | -------------- | ----------------------------------------------------- | ----- |
| 1 | `percentage` | Float 0..1 of the whole book | Universal. The only field every client can supply. |
| 2 | `context_text` | Up to 100 chars, whitespace-normalized, starting at the anchor | The verification anchor. The server extracts the text at the submitted structural anchor and compares. |
| 3 | `epubcfi` | Standard wrapped CFI, terminals in UTF-16 code units | Optional structural anchor. KOReader submits a CRE xpointer and the server converts it. |
Tier 2 is what makes the system self-correcting: percentages alone
cannot distinguish "the reader's locator is right" from "the reader
silently reported wherever it is currently scrolled".
## Verification and healing (ingest side)
On every progress save for a convertible reflowable book that carries a
`context_text`, the server:
1. Parses the submitted `epubcfi` (if any) and resolves it against the
book's own XHTML.
2. Extracts the text at the resolved anchor and cross-checks it with the
submitted `context_text`.
3. On a mismatch — or an unresolvable anchor, or no anchor at all —
heals the position by text search, using the submitted `percentage`
to disambiguate repeated phrases. The healed CFI and a recomputed
percentage are stored in place of the submitted ones.
4. Refreshes the book-wide `character_offset` column from the verified
anchor on every verified save, so it never goes stale behind the
anchor.
If verification fails outright (book file unreadable, context not found
and percentage cannot disambiguate), the error is logged and the
submission is stored as-is — verification never rejects a save, it only
corrects.
## The restore handles
`GET /api/media-items/:id/progress` serves, alongside the raw stored
fields, the server-derived handles for reflowable books with a
resolvable anchor:
| Field | Currency | Meaning |
| ------------- | ----------------------- | ------- |
| `anchor_href` | — | The spine document the anchor lives in (e.g. `1984.xhtml`). **This is how a client finds the right resource.** |
| `css_selector`| — | Body-relative `tag:nth-child(k)` chain of the anchor's block element (e.g. `body>div:nth-child(4)>p:nth-child(29)`). |
| `char_offset` | UTF-16 code units | Offset of the anchor within the concatenated text of that block. |
| `epubcfi` | UTF-16 terminals | The stored canonical CFI — re-served healed if the GET-time re-verification improved it. |
| `context_text`| — | The stored verification context (≤100 normalized chars from the anchor). |
**Recommended restore sequence for a client:**
1. Open the book and jump to the stored `percentage` (coarse floor).
2. Resolve `anchor_href` against your own spine (an ends-with match on
document hrefs) and open that document if you are not already there.
3. Query `css_selector` in that document, walk its text nodes counting
UTF-16 units to `char_offset`, and scroll that position into view.
4. If the anchor cannot be measured (renderer-specific laziness), the
percentage floor stands.
## SPINE NUMBERING HAZARD — read this before parsing a stored CFI
**`epubcfi` spine steps index the OPF spine AS WRITTEN, including
`linear="no"` items.** Several rendering engines (notably readium)
number their reading order EXCLUDING `linear="no"` items. When a book's
cover (or any other item) is `linear="no"`, the two numberings differ by
a constant offset from that item onward — a client resolving a stored
CFI's spine step against its own numbering lands in the WRONG DOCUMENT.
This is not hypothetical: "1984" epubs commonly have a `linear="no"`
cover, which makes readium spine 0 = OPF spine 1. The server heals such
numbering mismatches at ingest (that is what the context check is for),
but the durable rule for client authors is:
> **Never resolve a stored CFI's spine step yourself.** Resolve the
> document by `anchor_href`, then land with `css_selector` +
> `char_offset`. Treat the CFI as opaque server currency.
## Offset currencies
Two counting systems are in play, and they are deliberately kept apart:
| Quantity | Currency | Why |
| -------- | -------- | --- |
| CFI terminal offsets (`…/1:456`) | UTF-16 code units | The EPUB CFI spec, and what every client observes (JavaScript `.length`). |
| `char_offset` (block-relative handle) | UTF-16 code units | Same reason — clients walk DOM text with JS semantics. |
| KOReader CRE `text().N` offsets | UTF-16 code units | crengine is UCS-16 internally. |
| `character_offset` (book-wide column) | Unicode runes | Internal, consistent with `total_characters` and the percentage derivations. |
For all-BMP text the two currencies are identical. They diverge on
astral-plane characters (emoji, rare CJK ideographs): one rune, two
UTF-16 units. The server converts at every wire boundary; internal
arithmetic never crosses.
## `context_text` rules
- Starts at the anchor position (it may begin mid-word).
- Whitespace-normalized (all runs of whitespace collapse to single
spaces).
- At most 100 characters.
- Comparison is containment-based (client and server suffixes of the
same block verify in either direction); contexts shorter than 12
chars never match.
## Engine-specific notes
- **readium-based clients** (the Android app): submit
percentage + `context_text` + a CFI generated from the laid-out
WebView. Their locally-generated CFI spine steps use readium
numbering — the server heals the difference; nothing to do.
- **Web (foliate)**: submit all three tiers; foliate CFIs are the same
currency the server stores.
- **KOReader**: submits a CRE xpointer in the `epubcfi` field of the
device payload (historical field name; it is a CRE xpointer, not a
CFI). The server converts CRE → canonical at ingest and canonical →
CRE on pull (`koreader_xpointer` in the metadata response).
- **Kobo**: submits kepub CFI locators, converted server-side the same
way.
- **Fixed-layout content** (PDF/CBZ): the page index is the canonical
locator; CFI/xpointer are meaningless and neither submitted nor
served.
@@ -0,0 +1,91 @@
# Update Media Item Progress
Submit reading progress for a media item. This is the position-authority
ingest point: the server independently verifies the submission against
the book and heals it when the client's projection is wrong.
**Endpoint**: `PUT /api/media-items/:id/progress`
**Auth**: Required (Bearer token)
**Content-Type**: `application/json`
See the [Position Contract](position-contract.md) for the three-tier
submission model, the verification/healing semantics, and the offset
currencies.
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | ---------------- |
| id | string (UUID) | Yes | Media item UUID |
## Request Body
All fields are optional; submit what your renderer can observe.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `percentage` | float | Position as a fraction of the whole book (0..1). Tier 1 — the universal field. |
| `context_text` | string | Up to 100 whitespace-normalized chars starting at the anchor. Tier 2 — enables verification and healing. |
| `epubcfi` | string | Standard wrapped CFI (UTF-16 terminals). Tier 3 — the structural anchor. Note: the server re-derives the stored canonical CFI; a client's own spine numbering is healed if it disagrees with the OPF spine. |
| `character_offset` | int | Book-wide rune offset. Accepted but recomputed server-side from the verified anchor on every verified save. |
| `current_page`, `total_pages` | int | Fixed-layout position. For fixed-layout formats the page index is the canonical locator. |
| `chapter`, `chapter_progress` | int, float | Chapter index and within-chapter fraction. |
| `reading_mode` | string | `paged` or `scrolled` (fixed-layout reader state). |
| `zoom_level`, `scroll_position_x`, `scroll_position_y` | float | Fixed-layout viewport state. |
### Example Request (reflowable, all three tiers)
```http
PUT /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
```
```json
{
"percentage": 0.0415,
"context_text": "was at war with one of these Powers it was generally at peace with the other. But what was strange w",
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)"
}
```
### Example Request (fixed-layout)
```json
{
"percentage": 0.15,
"current_page": 30,
"total_pages": 194,
"reading_mode": "paged"
}
```
## Behavior
- The submission is verified against the book's XHTML when
`context_text` is present and the book is convertible reflowable: the
CFI is resolved, the text at the anchor is compared with
`context_text`, and mismatches heal by text search (percentage
disambiguates repeats). See [Position Contract](position-contract.md).
- **Anti-clobber guard**: a submission with `percentage < 0.005` is
ignored with `{"status": "ignored"}` when the stored row already holds
a percentage above `0.01` — re-opening a book at its first page does
not wipe real progress.
- The response is the stored row after verification, including the
healed `epubcfi` and the refreshed `character_offset` when
verification ran.
## Response (200 OK)
The saved progress row (same shape as
[GET](get_media_progress.md), minus the GET-time handles). The
`epubcfi` and `percentage` in the response are the server-verified
values, which may differ from the submitted ones when healing occurred.
## Error Responses
| Code | Description |
| ---- | ----------- |
| 400 | Invalid request data |
| 401 | Invalid or expired token |
| 500 | Database error |
@@ -1,68 +0,0 @@
# Update Reading Progress
Update reading progress for a media item. This will sync across all devices via WebSocket.
**Endpoint**: `PUT /api/media-items/{media_id}/progress`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | --------------- |
| media_id | string | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
| --------------------------- | ------- | -------- | ------------------------------------------------- |
| source | string | Yes | Progress source (e.g., "web", "koreader", "kobo") |
| location | object | Yes | Location information |
| location.percentage | float | No | Progress percentage (0-1) |
| location.epubcfi | string | No | EPUB CFI location |
| location.character | integer | No | Character offset |
| location.chapter | integer | No | Chapter number |
| location.page | integer | No | Current page |
| location.total_pages | integer | No | Total pages |
| device_metadata | object | No | Device metadata |
| device_metadata.device_type | string | No | Device type |
| device_metadata.user_agent | string | No | User agent string |
### Example Request
```json
{
"source": "web",
"location": {
"percentage": 0.45678,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": 15432,
"chapter": 3,
"page": 89,
"total_pages": 200
},
"device_metadata": {
"device_type": "web",
"user_agent": "Mozilla/5.0..."
}
}
```
## Response (200 OK)
```json
{
"sync_status": "success",
"progress_updated": true,
"devices_notified": ["device-1", "device-2"],
"broadcast": true
}
```
## Error Responses
| Code | Description |
| ---- | ------------------------ |
| 400 | Invalid location data |
| 401 | Invalid or expired token |
| 404 | Media item not found |
@@ -1,56 +0,0 @@
# Update Universal Progress
Update universal reading progress for a media item.
**Endpoint**: `POST /api/progress/:id`
**Auth**: Required
**Content-Type**: `application/json`
## Path Parameters
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | --------------- |
| id | string (UUID) | Yes | Media item UUID |
## Request Body
| Field | Type | Required | Description |
| ---------- | ------------- | -------- | ------------------------------------------- |
| percentage | float | No | Progress percentage (0-100) |
| position | integer | No | Current position in bytes |
| page | integer | No | Current page number |
| finished | boolean | No | Whether the book is finished |
| device_id | string (UUID) | No | Device UUID (optional, for tracking source) |
### Example Request
```json
{
"percentage": 75.5,
"position": 1234,
"page": 150,
"finished": false,
"device_id": "device-uuid"
}
```
## Response (200 OK)
```json
{
"media_item_id": "uuid",
"percentage": 75.5,
"position": 1234,
"page": 150,
"finished": false,
"updated_at": "2026-02-08T10:00:00Z"
}
```
## Error Responses
| Code | Description |
| ---- | ------------------------ |
| 400 | Invalid request data |
| 401 | Invalid or expired token |
| 404 | Media item not found |
+2
View File
@@ -77,6 +77,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
| **Deploy Bookhoard** | [Operations Portal → Troubleshooting](operations/troubleshooting.md) |
| **Contribute code** | [Contributing Portal → Development Guide](contributing/development.md) |
| **Understand sync** | [User Portal → Sync Guide](user/sync-guide.md) |
| **Read in the browser** | [User Portal → Web Reader Guide](user/reader-guide.md) |
---
@@ -90,6 +91,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
| ...set up KOReader? | [KOReader Setup Guide](user/devices/koreader-setup.md) |
| ...use a Kobo? | [Kobo Setup Guide](user/devices/kobo-setup.md) - native sync coming soon; KOReader works today |
| ...understand sync? | [Sync Guide](user/sync-guide.md) |
| ...read in the browser? | [Web Reader Guide](user/reader-guide.md) - reading, highlights, notes |
| ...resolve conflicts? | [Sync Guide](user/sync-guide.md) - Managing Conflicts |
| ...troubleshoot deployment? | [Troubleshooting Guide](operations/troubleshooting.md) |
| ...use the API? | [API Reference](developer/api-reference.md) |
+81
View File
@@ -0,0 +1,81 @@
# Web Reader Guide
Bookhoard's built-in web reader works in any modern browser — desktop or
mobile — and keeps your position, highlights, bookmarks, and notes in sync
with the Android app and KOReader.
## Opening a Book
From your library or book page, click **Read**. The reader opens in an
immersive view with the book's cover, title, and chapter in the top bar
(EPUB) or page number (PDF/comics).
## Navigation
### On a phone or tablet (touch)
| Gesture | Action |
|---|---|
| **Swipe** left / right | Turn the page |
| **Tap** once | Toggle the reader menu (headers, settings, drawers) |
| **Tap** in a corner zone | With the menu open: jump back / forward by chapter |
Paging is swipe-only by design: taps are reserved for the menu so a page
turn can never fire while you're trying to select text.
### On desktop
- Scroll wheel, trackpad, arrow keys, or **Page Up / Page Down** to page
- Click the progress bar (bottom, with the menu open) to jump
- Contents drawer for the table of contents; Pages grid for PDF/comics
## Text Selection & Highlights
### Selecting text (touch)
1. **Press and hold** a word (~half a second) until it highlights
2. **Drag** to extend the selection word by word — it stays on the
visible page
3. **Drag the round handles** (EPUB) to fine-tune the start and end
points
4. Lift your finger and pause — the highlight menu appears **below** the
selection, underneath the browser's own copy menu
The browser's address bar hides while a selection is active to give the
page room; it returns as soon as the selection is cleared.
### Creating a highlight or note
With the selection menu open:
- **Color dots** — tap one to highlight immediately in that color
- **Pencil** — opens a note; write it and tap **Highlight with note**
to save the highlight and its note together
- **Copy** — copies the selected text (works even over plain HTTP on
your LAN)
- While the note editor is open, you can still change the color — the
popover stays put until you save or cancel
### Editing or removing a highlight
Tap anywhere on a painted highlight to reopen it in the menu. From there
you can change its color, edit the note (pencil → **Save note**), or
delete it (trash icon). Tapping anywhere else closes the menu.
### PDFs and comics
Text selection works the same way on PDFs. Highlights are stored as
page-anchored rectangles and sync to the Android app. Comics/manga
libraries disable annotation (bookmarks still work).
## Notes & Bookmarks
The **annotations drawer** (right side, from the open menu) lists your
highlights, standalone notes, and bookmarks for the book — tap any entry
to jump to it.
## Where Reading Position Is Stored
Position, highlights, bookmarks, and notes sync automatically through the
server (see the [Sync Guide](sync-guide.md)). Close the tab whenever you
like — the reader resumes exactly where you left off on any device.
+8
View File
@@ -36,6 +36,14 @@ Learn how to configure your e-reader devices to sync with Bookhoard:
- Troubleshooting
- Best practices
## 📖 Reading
- **[Web Reader Guide](reader-guide.md)** - Reading and annotating in the browser
- Touch navigation (swipe to page, tap for menu)
- Selecting text: long-press, drag, handles
- Highlights, notes, copy, and editing
- PDFs and comics
## 🎨 Frontend Guide
**[Frontend Guide](frontend-guide.md)** - Learn how to use the Bookhoard web interface
+26
View File
@@ -1,5 +1,7 @@
github.com/ArcadiaLin/go-epub v0.1.1 h1:13roe62tarrZ1Y1QTxE+Bzd/NKlChhRzegLVmU5Hgws=
github.com/ArcadiaLin/go-epub v0.1.1/go.mod h1:GY09AG6jnEbsYytkw6VeICLOt25F1GQDGXjYS7cxNhU=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
@@ -12,8 +14,13 @@ github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -22,6 +29,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
@@ -72,16 +81,23 @@ github.com/labstack/echo/v5 v5.1.0 h1:MvIRydoN+p9cx/zq8Lff6YXqUW2ZaEsOMISzEGSMrB
github.com/labstack/echo/v5 v5.1.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas=
github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -91,6 +107,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -98,6 +116,8 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -118,16 +138,22 @@ golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+15 -1
View File
@@ -259,6 +259,12 @@ type Querier interface {
GetMediaItemFormats(ctx context.Context, mediaItemID pgtype.UUID) ([]MediaItemFormats, error)
// Per-item user-data counts, used when choosing which duplicate copy to keep
GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtype.UUID) (GetMediaItemUsageCountsRow, error)
// Cross-library move detection: identical content registered in a different
// library of the same type (ebooks stay with ebooks, comics with comics).
// Candidates may be active copies (deliberate duplicates - leave alone) or
// hidden/archived rows whose file vanished (move candidates - the scanner
// verifies disk state before repointing).
GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error)
GetMediaNote(ctx context.Context, id pgtype.UUID) (MediaNotes, error)
// ============================================
// ANNOTATION SYNC QUERIES (notes)
@@ -349,7 +355,10 @@ type Querier interface {
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
// Items hidden from libraries: either missing from disk (waiting on the
// second scan that archives them) or already archived and pending purge.
// Feeds the admin archived-items page.
// Feeds the admin archived-items page. match_* columns identify an active
// item with identical content (same SHA-256) so the UI can offer merging the
// archived row's reading data into its active twin instead of waiting for a
// file that will never return.
ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error)
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
@@ -374,6 +383,11 @@ type Querier interface {
// a move. Repoints the row (and refreshes file-level fields) so reading
// history follows the book; archive/missing state is cleared separately.
MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error
// Cross-library move: identical content reappeared in a different library of
// the same type while the old file vanished. Repoints the row so reading
// history, annotations and collections follow the book; archive/missing
// state is cleared. library_type_name is refreshed by trigger.
MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) error
// Manual bulk purge from the library admin page.
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
// Retention sweep at library-scan time: hard-delete archived items older
+253 -2
View File
@@ -2623,6 +2623,8 @@ SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_
FROM collection_items ci
JOIN media_items mi ON ci.media_item_id = mi.id
WHERE ci.collection_id = $1
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY ci.added_at DESC
`
@@ -2676,6 +2678,8 @@ SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.fi
INNER JOIN collection_items ci ON ci.media_item_id = mi.id
WHERE ci.collection_id = $1
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY ci.added_at DESC
LIMIT $3
`
@@ -2982,6 +2986,8 @@ INNER JOIN (
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY rp.last_read_at DESC
LIMIT $3
`
@@ -4378,6 +4384,8 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID)
const GetLibraryItems = `-- name: GetLibraryItems :many
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.created_at DESC
`
@@ -5694,6 +5702,184 @@ func (q *Queries) GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtyp
return i, err
}
const GetMediaItemsBySHA256AndSameLibraryType = `-- name: GetMediaItemsBySHA256AndSameLibraryType :many
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, lt.name AS library_type_name
FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
JOIN libraries target_lib ON target_lib.id = $1
WHERE mi.file_sha256 = $2
AND mi.library_id <> $1
AND lt.id = target_lib.library_type_id
ORDER BY (mi.archived_at IS NOT NULL) DESC,
mi.missing_scan_count DESC,
mi.created_at ASC
`
type GetMediaItemsBySHA256AndSameLibraryTypeParams struct {
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
}
type GetMediaItemsBySHA256AndSameLibraryTypeRow struct {
ID pgtype.UUID `db:"id" json:"id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
Series pgtype.Text `db:"series" json:"series"`
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
Tags []string `db:"tags" json:"tags"`
Asin pgtype.Text `db:"asin" json:"asin"`
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
Publisher pgtype.Text `db:"publisher" json:"publisher"`
Contributors []string `db:"contributors" json:"contributors"`
Language pgtype.Text `db:"language" json:"language"`
Edition pgtype.Text `db:"edition" json:"edition"`
PageCount pgtype.Int4 `db:"page_count" json:"page_count"`
Genre pgtype.Text `db:"genre" json:"genre"`
CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"`
GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"`
OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"`
GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"`
AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
FormatGroup string `db:"format_group" json:"format_group"`
FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"`
IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"`
HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"`
TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"`
ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"`
EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"`
RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"`
KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"`
KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"`
MangaType pgtype.Text `db:"manga_type" json:"manga_type"`
ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"`
SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"`
Volume pgtype.Int4 `db:"volume" json:"volume"`
Imprint pgtype.Text `db:"imprint" json:"imprint"`
AgeRating pgtype.Text `db:"age_rating" json:"age_rating"`
WebUrl pgtype.Text `db:"web_url" json:"web_url"`
StoryArc pgtype.Text `db:"story_arc" json:"story_arc"`
IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"`
MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"`
CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"`
AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"`
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
}
// Cross-library move detection: identical content registered in a different
// library of the same type (ebooks stay with ebooks, comics with comics).
// Candidates may be active copies (deliberate duplicates - leave alone) or
// hidden/archived rows whose file vanished (move candidates - the scanner
// verifies disk state before repointing).
func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error) {
rows, err := q.db.Query(ctx, GetMediaItemsBySHA256AndSameLibraryType, arg.LibraryID, arg.FileSha256)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetMediaItemsBySHA256AndSameLibraryTypeRow{}
for rows.Next() {
var i GetMediaItemsBySHA256AndSameLibraryTypeRow
if err := rows.Scan(
&i.ID,
&i.LibraryID,
&i.Title,
&i.Author,
&i.Isbn,
&i.Description,
&i.FilePath,
&i.FileSize,
&i.MimeType,
&i.CoverImagePath,
&i.Series,
&i.SeriesNumber,
&i.Tags,
&i.Asin,
&i.DatePublished,
&i.Publisher,
&i.Contributors,
&i.Language,
&i.Edition,
&i.PageCount,
&i.Genre,
&i.CopyrightYear,
&i.GoodreadsID,
&i.OpenlibraryID,
&i.GoogleBooksID,
&i.AddedByAdminID,
&i.CreatedAt,
&i.ImportedAt,
&i.UpdatedAt,
&i.FormatGroup,
&i.FormatMimetype,
&i.IsReflowable,
&i.HasFixedLayout,
&i.TotalCharacters,
&i.ChapterCount,
&i.EntitlementID,
&i.RevisionNumber,
&i.KoboContentID,
&i.KoboMetadata,
&i.MangaType,
&i.ReadingDirection,
&i.SeriesCount,
&i.Volume,
&i.Imprint,
&i.AgeRating,
&i.WebUrl,
&i.StoryArc,
&i.IsBlackAndWhite,
&i.MetadataNotes,
&i.CommunityRating,
&i.AlternateInfo,
&i.ScanInformation,
&i.Summary,
&i.MetadataOverrides,
&i.MissingScanCount,
&i.ArchivedAt,
&i.ChapterMetadata,
&i.LibraryTypeName,
&i.TagsSearch,
&i.ContributorsSearch,
&i.FileSha256,
&i.OpfIdentifier,
&i.OpfUuid,
&i.HashConfidence,
&i.LibraryTypeName_2,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetMediaNote = `-- name: GetMediaNote :one
SELECT id, media_item_id, user_id, content, position, created_at, updated_at, percentage_location, character_start, character_end, epubcfi_location, chapter_reference, paragraph_reference, device_sync_data, dedup_key, last_modified_at, last_modified_source, deleted, deleted_at FROM media_notes WHERE id = $1
`
@@ -5909,6 +6095,8 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) {
const GetNotStartedItems = `-- name: GetNotStartedItems :many
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND NOT EXISTS (
SELECT 1 FROM reading_progress rp
WHERE rp.media_item_id = mi.id
@@ -6312,6 +6500,8 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams
const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
LIMIT $2
`
@@ -6416,6 +6606,8 @@ INNER JOIN (
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY rp.last_read_at DESC
LIMIT $3
`
@@ -8419,10 +8611,28 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]
}
const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name,
twin.id AS match_id, COALESCE(twin.title, '') AS match_title,
twin_library.name AS match_library_name
FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
LEFT JOIN LATERAL (
SELECT t.id, t.title, t.library_id
FROM media_items t
JOIN libraries tl ON t.library_id = tl.id
JOIN library_types tlt ON tl.library_type_id = tlt.id
WHERE t.file_sha256 = mi.file_sha256
AND t.id <> mi.id
AND t.archived_at IS NULL
AND t.missing_scan_count = 0
ORDER BY (tlt.id = lt.id) DESC,
(tl.id = l.id) DESC,
(SELECT COUNT(*) FROM reading_progress rp WHERE rp.media_item_id = t.id) DESC,
t.created_at ASC
LIMIT 1
) twin ON true
LEFT JOIN libraries twin_library ON twin_library.id = twin.library_id
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC
`
@@ -8494,11 +8704,17 @@ type ListHiddenMediaItemsRow struct {
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
LibraryName string `db:"library_name" json:"library_name"`
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
MatchID pgtype.UUID `db:"match_id" json:"match_id"`
MatchTitle string `db:"match_title" json:"match_title"`
MatchLibraryName pgtype.Text `db:"match_library_name" json:"match_library_name"`
}
// Items hidden from libraries: either missing from disk (waiting on the
// second scan that archives them) or already archived and pending purge.
// Feeds the admin archived-items page.
// Feeds the admin archived-items page. match_* columns identify an active
// item with identical content (same SHA-256) so the UI can offer merging the
// archived row's reading data into its active twin instead of waiting for a
// file that will never return.
func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error) {
rows, err := q.db.Query(ctx, ListHiddenMediaItems)
if err != nil {
@@ -8575,6 +8791,9 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt
&i.HashConfidence,
&i.LibraryName,
&i.LibraryTypeName_2,
&i.MatchID,
&i.MatchTitle,
&i.MatchLibraryName,
); err != nil {
return nil, err
}
@@ -9999,6 +10218,38 @@ func (q *Queries) MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFi
return err
}
const MoveMediaItemToLibrary = `-- name: MoveMediaItemToLibrary :exec
UPDATE media_items
SET library_id = $2,
file_path = $3,
file_size = $4,
archived_at = NULL,
missing_scan_count = 0,
updated_at = NOW()
WHERE id = $1
`
type MoveMediaItemToLibraryParams struct {
ID pgtype.UUID `db:"id" json:"id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
}
// Cross-library move: identical content reappeared in a different library of
// the same type while the old file vanished. Repoints the row so reading
// history, annotations and collections follow the book; archive/missing
// state is cleared. library_type_name is refreshed by trigger.
func (q *Queries) MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) error {
_, err := q.db.Exec(ctx, MoveMediaItemToLibrary,
arg.ID,
arg.LibraryID,
arg.FilePath,
arg.FileSize,
)
return err
}
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
DELETE FROM media_items
WHERE archived_at IS NOT NULL
+69 -2
View File
@@ -328,6 +328,20 @@ WHERE id = $1;
UPDATE media_items SET archived_at = NOW(), updated_at = NOW()
WHERE id = $1;
-- name: MoveMediaItemToLibrary :exec
-- Cross-library move: identical content reappeared in a different library of
-- the same type while the old file vanished. Repoints the row so reading
-- history, annotations and collections follow the book; archive/missing
-- state is cleared. library_type_name is refreshed by trigger.
UPDATE media_items
SET library_id = $2,
file_path = $3,
file_size = $4,
archived_at = NULL,
missing_scan_count = 0,
updated_at = NOW()
WHERE id = $1;
-- name: ClearMediaItemArchive :exec
-- File is back on disk (by path or content hash): restore visibility and
-- reset the missing-scan counter. No-op for items that were never archived.
@@ -353,11 +367,32 @@ SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL;
-- name: ListHiddenMediaItems :many
-- Items hidden from libraries: either missing from disk (waiting on the
-- second scan that archives them) or already archived and pending purge.
-- Feeds the admin archived-items page.
SELECT mi.*, l.name as library_name, lt.name as library_type_name
-- Feeds the admin archived-items page. match_* columns identify an active
-- item with identical content (same SHA-256) so the UI can offer merging the
-- archived row's reading data into its active twin instead of waiting for a
-- file that will never return.
SELECT mi.*, l.name as library_name, lt.name as library_type_name,
twin.id AS match_id, COALESCE(twin.title, '') AS match_title,
twin_library.name AS match_library_name
FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
LEFT JOIN LATERAL (
SELECT t.id, t.title, t.library_id
FROM media_items t
JOIN libraries tl ON t.library_id = tl.id
JOIN library_types tlt ON tl.library_type_id = tlt.id
WHERE t.file_sha256 = mi.file_sha256
AND t.id <> mi.id
AND t.archived_at IS NULL
AND t.missing_scan_count = 0
ORDER BY (tlt.id = lt.id) DESC,
(tl.id = l.id) DESC,
(SELECT COUNT(*) FROM reading_progress rp WHERE rp.media_item_id = t.id) DESC,
t.created_at ASC
LIMIT 1
) twin ON true
LEFT JOIN libraries twin_library ON twin_library.id = twin.library_id
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC;
@@ -1898,6 +1933,24 @@ SELECT * FROM media_items WHERE file_sha256 = $1;
-- name: GetMediaItemBySHA256AndLibrary :one
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2;
-- name: GetMediaItemsBySHA256AndSameLibraryType :many
-- Cross-library move detection: identical content registered in a different
-- library of the same type (ebooks stay with ebooks, comics with comics).
-- Candidates may be active copies (deliberate duplicates - leave alone) or
-- hidden/archived rows whose file vanished (move candidates - the scanner
-- verifies disk state before repointing).
SELECT mi.*, lt.name AS library_type_name
FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id
JOIN libraries target_lib ON target_lib.id = sqlc.arg('library_id')
WHERE mi.file_sha256 = sqlc.arg('file_sha256')
AND mi.library_id <> sqlc.arg('library_id')
AND lt.id = target_lib.library_type_id
ORDER BY (mi.archived_at IS NOT NULL) DESC,
mi.missing_scan_count DESC,
mi.created_at ASC;
-- List all media items sharing a SHA-256 hash within a library (hash conflict group)
-- name: ListMediaItemsBySHA256AndLibrary :many
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path;
@@ -2143,6 +2196,8 @@ SELECT ci.*, mi.title, mi.author, mi.cover_image_path, mi.library_id
FROM collection_items ci
JOIN media_items mi ON ci.media_item_id = mi.id
WHERE ci.collection_id = $1
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY ci.added_at DESC;
-- Get collections for book
@@ -2449,12 +2504,16 @@ INNER JOIN (
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY rp.last_read_at DESC
LIMIT sqlc.narg('limit');
-- name: GetRecentlyAddedItems :many
SELECT mi.* FROM media_items mi
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
LIMIT sqlc.narg('limit');
@@ -2468,12 +2527,16 @@ INNER JOIN (
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY rp.last_read_at DESC
LIMIT sqlc.narg('limit');
-- name: GetNotStartedItems :many
SELECT mi.* FROM media_items mi
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
AND NOT EXISTS (
SELECT 1 FROM reading_progress rp
WHERE rp.media_item_id = mi.id
@@ -2488,12 +2551,16 @@ SELECT mi.*, ci.excluded FROM media_items mi
INNER JOIN collection_items ci ON ci.media_item_id = mi.id
WHERE ci.collection_id = sqlc.narg('collection_id')
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY ci.added_at DESC
LIMIT sqlc.narg('limit');
-- name: GetLibraryItems :many
SELECT mi.* FROM media_items mi
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.created_at DESC;
-- name: GetDistinctSeries :many
+123 -20
View File
@@ -6,11 +6,16 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v5"
"github.com/skip2/go-qrcode"
@@ -106,7 +111,14 @@ type PendingRegistration struct {
SyncEndpoints map[string]string
}
var pendingRegistrations = make(map[string]*PendingRegistration)
// pendingRegistrations holds in-flight (unapproved) device registrations.
// HTTP handlers touch it from multiple goroutines — every access must hold
// pendingMu (Go maps are not safe for concurrent use; a racing write is a
// runtime fatal, not an error).
var (
pendingRegistrations = make(map[string]*PendingRegistration)
pendingMu sync.Mutex
)
func (h *DeviceHandler) InitiateRegistration(c *echo.Context) error {
req := DeviceRegistrationRequest{}
@@ -130,7 +142,9 @@ func (h *DeviceHandler) InitiateRegistration(c *echo.Context) error {
CreatedAt: time.Now(),
}
pendingMu.Lock()
pendingRegistrations[registrationID] = registration
pendingMu.Unlock()
authURL := fmt.Sprintf("%s/devices/approve/%s", h.cfg.BaseURL, registrationID)
@@ -167,26 +181,34 @@ func (h *DeviceHandler) CheckRegistrationStatus(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
}
pendingMu.Lock()
registration, exists := pendingRegistrations[req.RegistrationID]
if !exists {
pendingMu.Unlock()
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
}
if time.Now().After(registration.ExpiresAt) {
delete(pendingRegistrations, req.RegistrationID)
pendingMu.Unlock()
return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"})
}
if registration.Approved {
delete(pendingRegistrations, req.RegistrationID)
return c.JSON(http.StatusOK, DeviceAuthStatusResponse{
// Snapshot under the lock: the approver wrote these fields, and
// the row is gone from the map — no other reader/writer remains.
resp := DeviceAuthStatusResponse{
Status: "approved",
AuthToken: registration.AuthToken,
DeviceID: registration.DeviceID,
SyncEndpoints: registration.SyncEndpoints,
})
}
pendingMu.Unlock()
return c.JSON(http.StatusOK, resp)
}
pendingMu.Unlock()
return c.JSON(http.StatusOK, DeviceAuthStatusResponse{
Status: "pending",
@@ -552,17 +574,21 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
registrationID := c.Param("registration_id")
pendingMu.Lock()
registration, exists := pendingRegistrations[registrationID]
if !exists {
pendingMu.Unlock()
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
}
if time.Now().After(registration.ExpiresAt) {
delete(pendingRegistrations, registrationID)
pendingMu.Unlock()
return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"})
}
if registration.Approved {
pendingMu.Unlock()
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "device already approved",
"device_name": registration.DeviceName,
@@ -570,6 +596,7 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
"approved": true,
})
}
pendingMu.Unlock()
authToken, err := generateDeviceToken()
if err != nil {
@@ -577,23 +604,87 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
}
pgUserID := pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true}
syncEnabled := pgtype.Bool{Bool: true, Valid: true}
autoSync := pgtype.Bool{Bool: true, Valid: true}
syncFreq := pgtype.Int4{Int32: 5, Valid: true}
device, err := h.db.CreateDevice(c.Request().Context(), database.CreateDeviceParams{
UserID: pgUserID,
DeviceName: registration.DeviceName,
DeviceType: registration.DeviceType,
DeviceIdentifier: registration.DeviceIdentifier,
AuthToken: authToken,
SyncEnabled: syncEnabled,
AutoSync: autoSync,
SyncFrequencyMinutes: syncFreq,
DeviceMetadata: []byte("{}"),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create device"})
// Re-approval: app reinstalls that preserve data (e.g. Android Studio
// installDebug over an existing install) re-register with the SAME
// device_identifier, but the devices row from the previous install
// still exists — device_identifier is UNIQUE, so a blind INSERT fails
// with a unique violation (the historical "approve returns 500 after
// reinstall" bug). Look the device up first and rotate its token
// instead; a fresh install MUST get fresh credentials, so the old
// token is invalidated either way.
rotateExisting := func(existing database.Devices) (database.Devices, bool, error) {
// The identifier is globally unique; a row owned by another user
// means cross-account identifier reuse — refuse it.
if !existing.UserID.Valid || existing.UserID.Bytes != pgUserID.Bytes {
return existing, false, nil
}
updated, err := h.db.UpdateDeviceAuthToken(c.Request().Context(),
database.UpdateDeviceAuthTokenParams{
ID: pgtype.UUID{Bytes: existing.ID.Bytes, Valid: true},
AuthToken: authToken,
})
return updated, true, err
}
existing, err := h.db.GetDeviceByIdentifier(c.Request().Context(),
registration.DeviceIdentifier)
var device database.Devices
switch {
case err == nil:
// Known device: rotate the token, keep the row (id unchanged —
// highlights/bookmarks/progress anchored to it stay valid).
var ok bool
device, ok, err = rotateExisting(existing)
if err != nil {
slog.Error("device re-approval failed",
"registration_id", registrationID, "error", err)
return c.JSON(http.StatusInternalServerError,
map[string]string{"error": "failed to approve device"})
}
if !ok {
return c.JSON(http.StatusConflict,
map[string]string{"error": "device identifier already registered to another user"})
}
case errors.Is(err, pgx.ErrNoRows):
device, err = h.db.CreateDevice(c.Request().Context(), database.CreateDeviceParams{
UserID: pgUserID,
DeviceName: registration.DeviceName,
DeviceType: registration.DeviceType,
DeviceIdentifier: registration.DeviceIdentifier,
AuthToken: authToken,
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
AutoSync: pgtype.Bool{Bool: true, Valid: true},
SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true},
DeviceMetadata: []byte("{}"),
})
if err != nil {
// Concurrent approve racing us onto the unique index: fall
// through to the rotate path for the winner's row.
var pgErr *pgconn.PgError
if asErr := errors.As(err, &pgErr); asErr && pgErr.Code == "23505" {
if winner, lerr := h.db.GetDeviceByIdentifier(
c.Request().Context(), registration.DeviceIdentifier); lerr == nil {
var ok bool
device, ok, err = rotateExisting(winner)
if err == nil && !ok {
return c.JSON(http.StatusConflict,
map[string]string{"error": "device identifier already registered to another user"})
}
}
}
}
if err != nil {
slog.Error("device creation failed",
"registration_id", registrationID, "error", err)
return c.JSON(http.StatusInternalServerError,
map[string]string{"error": "failed to create device"})
}
default:
slog.Error("device lookup failed",
"registration_id", registrationID, "error", err)
return c.JSON(http.StatusInternalServerError,
map[string]string{"error": "failed to look up device"})
}
syncEndpoints := map[string]string{}
@@ -607,11 +698,16 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
syncEndpoints["library"] = fmt.Sprintf("%s/api/sync/kobo/library", h.cfg.BaseURL)
}
// Publish the credentials under the map lock: the status poller
// snapshots these fields only after Approved flips, so the token can
// never be read half-written.
pendingMu.Lock()
registration.UserID = userUUID
registration.Approved = true
registration.AuthToken = authToken
registration.DeviceID = device.ID.Bytes
registration.SyncEndpoints = syncEndpoints
pendingMu.Unlock()
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "device approved successfully",
@@ -625,12 +721,15 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
func (h *DeviceHandler) RejectDevice(c *echo.Context) error {
registrationID := c.Param("registration_id")
pendingMu.Lock()
_, exists := pendingRegistrations[registrationID]
if !exists {
pendingMu.Unlock()
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
}
delete(pendingRegistrations, registrationID)
pendingMu.Unlock()
return c.JSON(http.StatusOK, map[string]string{
"message": "device registration rejected",
@@ -639,6 +738,7 @@ func (h *DeviceHandler) RejectDevice(c *echo.Context) error {
func (h *DeviceHandler) GetPendingRegistrationsData(c *echo.Context) ([]map[string]interface{}, error) {
registrations := []map[string]interface{}{}
pendingMu.Lock()
for _, reg := range pendingRegistrations {
if reg.UserID == (uuid.UUID{}) {
registrations = append(registrations, map[string]interface{}{
@@ -652,6 +752,7 @@ func (h *DeviceHandler) GetPendingRegistrationsData(c *echo.Context) ([]map[stri
})
}
}
pendingMu.Unlock()
return registrations, nil
}
@@ -664,6 +765,7 @@ func (h *DeviceHandler) ListPendingRegistrations(c *echo.Context) error {
}
registrations := []map[string]interface{}{}
pendingMu.Lock()
for _, reg := range pendingRegistrations {
if reg.UserID == userUUID || reg.UserID == (uuid.UUID{}) {
registrations = append(registrations, map[string]interface{}{
@@ -677,6 +779,7 @@ func (h *DeviceHandler) ListPendingRegistrations(c *echo.Context) error {
})
}
}
pendingMu.Unlock()
return c.JSON(http.StatusOK, map[string]interface{}{
"registrations": registrations,
+108
View File
@@ -953,6 +953,37 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
"last_sync_timestamp": progress.LastSyncTimestamp,
}
// Position authority: re-verify the stored anchor and derive the
// client-facing handles (cssSelector + anchor document href) server-
// side, so clients scroll to an address instead of parsing CFIs.
if progress.Epubcfi.Valid && wsync.IsConvertibleFormat(progress.FormatGroup) && mh.libraryService != nil {
contextText := ""
if progress.ContextText.Valid {
contextText = progress.ContextText.String
}
pct := 0.0
if progress.Percentage.Valid {
pct = progress.Percentage.Float64
}
if mediaItem, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}); err == nil {
if path, perr := mh.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath); perr == nil && path != "" {
if finalCFI, sel, href, charOff, _, _, _, verr := wsync.VerifyProgressAnchor(path, progress.Epubcfi.String, contextText, pct); verr == nil && sel != "" {
resp["css_selector"] = sel
resp["anchor_href"] = href
if charOff != nil {
resp["char_offset"] = *charOff
}
if finalCFI != "" {
resp["epubcfi"] = finalCFI
}
}
}
}
}
if progress.ContextText.Valid {
resp["context_text"] = progress.ContextText.String
}
return c.JSON(http.StatusOK, resp)
}
@@ -1354,6 +1385,83 @@ func (mh *MediaHandler) UnarchiveMediaItem(c *echo.Context) error {
return c.NoContent(http.StatusNoContent)
}
// MergeArchivedMediaItem handles POST /api/media-items/:id/merge (admin
// only). Merges an archived/missing item's reading data (progress,
// annotations, highlights, notes, bookmarks, ratings, collections, kobo
// shelves) into an active item with identical content (same SHA-256), then
// deletes the archived row. Covers the copy-then-delete-later workflow: the
// active copy exists, the archived twin holds the real reading history.
// Per-user collisions keep the active copy's data, mirroring hash-conflict
// resolution.
func (mh *MediaHandler) MergeArchivedMediaItem(c *echo.Context) error {
user := MustGetAuthenticatedUser(c)
if user.Role != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
}
sourceUUID, err := uuid.Parse(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
var req struct {
TargetID string `json:"target_id"`
}
if err := c.Bind(&req); err != nil || req.TargetID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "target_id is required"})
}
targetUUID, err := uuid.Parse(req.TargetID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid target_id"})
}
if sourceUUID == targetUUID {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot merge an item into itself"})
}
ctx := c.Request().Context()
source, err := mh.db.GetMediaItem(ctx, pgtype.UUID{Bytes: sourceUUID, Valid: true})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "archived item not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
if !source.ArchivedAt.Valid && source.MissingScanCount == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "source item is not archived or missing"})
}
target, err := mh.db.GetMediaItem(ctx, pgtype.UUID{Bytes: targetUUID, Valid: true})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "target item not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
if target.ArchivedAt.Valid || target.MissingScanCount > 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "target item is not active"})
}
if !source.FileSha256.Valid || source.FileSha256.String == "" ||
source.FileSha256.String != target.FileSha256.String {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "items do not share the same content hash"})
}
if err := mh.db.ReparentMediaItemChildren(ctx, database.ReparentMediaItemChildrenParams{
Column1: pgtype.UUID{Bytes: targetUUID, Valid: true},
Column2: pgtype.UUID{Bytes: sourceUUID, Valid: true},
}); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to merge reading data: " + err.Error()})
}
if err := mh.db.DeleteMediaItem(ctx, pgtype.UUID{Bytes: sourceUUID, Valid: true}); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to remove archived item: " + err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Merged reading progress, annotations, and collections into the active item",
})
}
// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
// (admin only). Hard-deletes every archived item (files missing from disk for
// 2+ scans) together with its reading history. The archive retention window
+288 -11
View File
@@ -90,6 +90,9 @@ func appendToken(url, token string) string {
// catalogMediaType is the OPDS media type for an acquisition catalog feed.
const catalogMediaType = "application/atom+xml;profile=opds-catalog;kind=acquisition"
// navFeedMediaType is the OPDS media type for a navigation catalog feed.
const navFeedMediaType = "application/atom+xml;profile=opds-catalog;kind=navigation"
// addCatalogPaginationLinks adds OPDS pagination links (self, start, first,
// previous, next, last) and OpenSearch paging metadata (totalResults,
// itemsPerPage, startIndex) to a feed based on the current page position.
@@ -172,8 +175,109 @@ func formatLabelFromPath(path string) string {
}
}
// GetDeviceCatalog returns the OPDS catalog feed for a device
// GetDeviceCatalog serves the OPDS root feed for a device.
//
// By default it returns a navigation feed: an "All Books" entry (the flat
// cross-library catalog) followed by one folder entry per visible library.
// OPDS clients render navigation entries as folders, so each library's
// books — and its library-scoped search — live under their own node instead
// of interleaving duplicate copies of the same book in one flat list.
// Passing ?all=1 serves the flat cross-library acquisition feed directly,
// preserving the pre-folders behavior.
func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
if c.QueryParam("all") == "1" {
return h.serveAllBooksCatalog(c)
}
deviceID := c.Param("deviceId")
// Get base URLs
_, opdsBaseURL, err := h.getBaseURLs(c)
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
}
// Parse device ID
deviceUUID, err := uuid.Parse(deviceID)
if err != nil {
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid device ID"))
}
// Verify device exists
device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
if err != nil {
return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
}
// Get user's visible libraries
userID := device.UserID.Bytes
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
}
token := h.getAuthToken(c)
feed := opds.NewFeed(
fmt.Sprintf("urn:uuid:%s", deviceID),
"Bookhoard Library",
)
// OpenSearch discovery: the root search stays global (all libraries).
searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID), token)
feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")
updated := time.Now().UTC().Format("2006-01-02T15:04:05Z")
// "All Books" first: the flat cross-library catalog (?all=1).
allBooks := opds.NewEntry(
fmt.Sprintf("urn:uuid:%s-all-books", deviceID),
"All Books",
"",
updated,
)
allBooks.AddLink(appendToken(fmt.Sprintf("%s/devices/%s/catalog?all=1", opdsBaseURL, deviceID), token), catalogMediaType, "subsection")
allBooks.SetSummary("Browse every book across all your libraries")
feed.AddEntry(allBooks)
// One folder per visible library, with live book counts.
counts := make(map[pgtype.UUID]int64)
if countRows, cerr := h.db.GetVisibleLibraryMediaCounts(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}); cerr == nil {
for _, row := range countRows {
counts[row.ID] = row.MediaCount
}
}
for _, lib := range libraries {
libUUID := uuid.UUID(lib.ID.Bytes).String()
title := lib.Name
if n, ok := counts[lib.ID]; ok {
title = fmt.Sprintf("%s (%d)", lib.Name, n)
}
entry := opds.NewEntry(
fmt.Sprintf("urn:uuid:%s-library-%s", deviceID, libUUID),
title,
"",
updated,
)
entry.AddLink(appendToken(fmt.Sprintf("%s/devices/%s/library/%s/catalog", opdsBaseURL, deviceID, libUUID), token), catalogMediaType, "subsection")
entry.SetSummary("Browse books in the " + lib.Name + " library")
feed.AddEntry(entry)
}
// Generate XML
xmlString, err := feed.GenerateXMLString()
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
}
c.Response().Header().Set("Content-Type", navFeedMediaType)
return c.String(http.StatusOK, xmlString)
}
// serveAllBooksCatalog is the original flat cross-library acquisition feed:
// every book from every visible library, paginated as one list.
func (h *OPDSHandler) serveAllBooksCatalog(c *echo.Context) error {
deviceID := c.Param("deviceId")
page := c.QueryParam("page")
@@ -216,8 +320,6 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
// Get user's visible libraries
userID := device.UserID.Bytes
userUUID := uuid.UUID(userID)
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
@@ -276,7 +378,23 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")
// Add entries
for _, item := range allItems {
h.addAcquisitionEntries(c.Request().Context(), feed, device, deviceID, allItems, baseURL, opdsBaseURL, token)
// Generate XML
xmlString, err := feed.GenerateXMLString()
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
}
c.Response().Header().Set("Content-Type", catalogMediaType)
return c.String(http.StatusOK, xmlString)
}
// addAcquisitionEntries fills an acquisition feed with book entries for the
// given items. Shared body of the all-books and per-library catalogs.
func (h *OPDSHandler) addAcquisitionEntries(ctx context.Context, feed *opds.Feed, device database.Devices, deviceID string, items []database.ListMediaItemsByLibraryRow, baseURL, opdsBaseURL, token string) {
userUUID := uuid.UUID(device.UserID.Bytes)
for _, item := range items {
bookUUID := uuid.UUID(item.ID.Bytes).String()
title := item.Title
author := ""
@@ -323,7 +441,7 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
}
// Add collections as categories
collections, err := h.db.GetCollectionsForBook(c.Request().Context(), pgtype.UUID{Bytes: item.ID.Bytes, Valid: true})
collections, err := h.db.GetCollectionsForBook(ctx, pgtype.UUID{Bytes: item.ID.Bytes, Valid: true})
if err == nil {
collectionScheme := fmt.Sprintf("%s/collections", baseURL)
for _, col := range collections {
@@ -335,6 +453,125 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
feed.AddEntry(entry)
}
}
// GetLibraryCatalog serves an acquisition feed scoped to one of the device
// owner's visible libraries. Its search link is library-scoped: OPDS clients
// only substitute {searchTerms}, so opening search from inside this folder
// searches within this library only.
func (h *OPDSHandler) GetLibraryCatalog(c *echo.Context) error {
deviceID := c.Param("deviceId")
libraryID := c.Param("libraryId")
page := c.QueryParam("page")
perPage := c.QueryParam("per_page")
includeFormat := c.QueryParam("include_format")
// Parse pagination parameters
pageNum := 1
if page != "" {
if num, err := strconv.Atoi(page); err == nil && num > 0 {
pageNum = num
}
}
perPageNum := h.opdsDefaultPageSize()
maxPerPage := h.opdsMaxPageSize()
if perPage != "" {
if num, err := strconv.Atoi(perPage); err == nil && num > 0 && num <= maxPerPage {
perPageNum = num
}
}
// Get base URLs
baseURL, opdsBaseURL, err := h.getBaseURLs(c)
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
}
// Parse IDs
deviceUUID, err := uuid.Parse(deviceID)
if err != nil {
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid device ID"))
}
libUUID, err := uuid.Parse(libraryID)
if err != nil {
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid library ID"))
}
// Verify device exists
device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
if err != nil {
return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
}
// The library must be visible to the device owner.
userID := device.UserID.Bytes
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
}
var library *database.GetUserVisibleLibrariesRow
for i := range libraries {
if libraries[i].ID.Bytes == libUUID {
library = &libraries[i]
break
}
}
if library == nil {
return c.XML(http.StatusForbidden, opds.NewErrorFeed("Library not accessible"))
}
// Get media items
items, err := h.db.ListMediaItemsByLibrary(c.Request().Context(), library.ID)
if err != nil {
items = []database.ListMediaItemsByLibraryRow{}
}
// Filter by format if specified
if includeFormat != "" {
filteredItems := []database.ListMediaItemsByLibraryRow{}
for _, item := range items {
if strings.EqualFold(item.FormatGroup, includeFormat) {
filteredItems = append(filteredItems, item)
}
}
items = filteredItems
}
// Pagination
totalItems := len(items)
startIdx := (pageNum - 1) * perPageNum
endIdx := startIdx + perPageNum
if startIdx >= totalItems {
items = []database.ListMediaItemsByLibraryRow{}
} else if endIdx > totalItems {
items = items[startIdx:]
} else {
items = items[startIdx:endIdx]
}
// Create OPDS feed
feed := opds.NewFeed(
fmt.Sprintf("urn:uuid:%s-library-%s", deviceID, libUUID),
library.Name,
)
token := h.getAuthToken(c)
catalogBase := fmt.Sprintf("%s/devices/%s/library/%s/catalog", opdsBaseURL, deviceID, libUUID)
addCatalogPaginationLinks(feed, catalogBase, pageNum, perPageNum, totalItems, token)
// Library-scoped search discovery: the template pins this library.
searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}&library_id=%s", opdsBaseURL, deviceID, libUUID), token)
feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")
// Up-link back to the root navigation feed.
rootURL := appendToken(fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID), token)
feed.AddLink(rootURL, navFeedMediaType, "up")
// Add entries
h.addAcquisitionEntries(c.Request().Context(), feed, device, deviceID, items, baseURL, opdsBaseURL, token)
// Generate XML
xmlString, err := feed.GenerateXMLString()
@@ -342,7 +579,7 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
}
c.Response().Header().Set("Content-Type", "application/atom+xml;profile=opds-catalog;kind=acquisition")
c.Response().Header().Set("Content-Type", catalogMediaType)
return c.String(http.StatusOK, xmlString)
}
@@ -377,20 +614,50 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
// Get user's visible libraries
userID := device.UserID.Bytes
_, err = h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
if err != nil {
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
}
// Optional library scope: library folder feeds advertise their own search
// link with this parameter, so opening search from inside a library
// folder searches within that library only.
var scopedLibraryID pgtype.UUID
scopedLibraryName := ""
if libParam := c.QueryParam("library_id"); libParam != "" {
parsed, perr := uuid.Parse(libParam)
if perr != nil {
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid library ID"))
}
for i := range libraries {
if libraries[i].ID.Bytes == parsed {
scopedLibraryID = libraries[i].ID
scopedLibraryName = libraries[i].Name
break
}
}
if !scopedLibraryID.Valid {
return c.XML(http.StatusForbidden, opds.NewErrorFeed("Library not accessible"))
}
}
token := h.getAuthToken(c)
// No query: serve the OpenSearch description document so clients can learn
// the search template (contains the {searchTerms} placeholder).
// the search URL template (contains the {searchTerms} placeholder).
if query == "" {
searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID), token)
template := fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID)
shortName := "Bookhoard"
description := "Search the Bookhoard library"
if scopedLibraryID.Valid {
template += "&library_id=" + scopedLibraryID.String()
shortName = "Bookhoard: " + scopedLibraryName
description = "Search the " + scopedLibraryName + " library"
}
searchURL := appendToken(template, token)
desc := opds.NewSearchDescription(
"Bookhoard",
"Search the Bookhoard library",
shortName,
description,
searchURL,
)
xmlString, err := desc.GenerateXMLString()
@@ -404,6 +671,7 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
// Search media items
allItems, err := h.db.SearchMediaItems(c.Request().Context(), database.SearchMediaItemsParams{
UserID: pgtype.UUID{Bytes: userID, Valid: true},
LibraryID: scopedLibraryID,
SearchPattern: pgtype.Text{String: "%" + query + "%", Valid: true},
Offset: pgtype.Int4{Int32: 0, Valid: true},
Limit: pgtype.Int4{Int32: 1000, Valid: true},
@@ -451,6 +719,15 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
entry.SetSummary(item.Description.String)
}
// Global (unscoped) search: label which library each copy lives in so
// duplicate copies of the same book are distinguishable.
if !scopedLibraryID.Valid && item.LibraryName != "" {
entry.AddCategory(baseURL+"/libraries", item.LibraryName)
if !item.Description.Valid || item.Description.String == "" {
entry.SetSummary("Library: " + item.LibraryName)
}
}
downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token)
entry.AddAcquisitionLink(downloadURL, resolveMimeType(item.MimeType, item.FormatMimetype))
+21
View File
@@ -102,6 +102,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
// Determine if panel detection should be enabled
enablePanelDetection := shouldEnablePanelDetection(libraryWithType.TypeName, mediaItem.FormatGroup)
treatAsComic := ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup)
// Get reading progress
var progress database.ReadingProgress
@@ -133,6 +134,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
"progress": progress,
"bookmarks": bookmarks,
"enable_panel_detection": enablePanelDetection,
"treat_as_comic": treatAsComic,
"format_group": mediaItem.FormatGroup,
"manga_type": mediaItem.MangaType,
"reading_direction": mediaItem.ReadingDirection,
@@ -639,6 +641,25 @@ func (h *ReaderHandler) ParseEbook(c *echo.Context) error {
})
}
// ShouldTreatAsComic reports whether a fixed-layout item that lives in a
// comics or manga library should get comic reader treatment: bookmarks
// only — no text-selection highlights, no annotations, no in-book search.
// Comic archives already behave this way; this flag extends it to fixed-
// layout PDFs shelved in comic libraries (scanned or official manga PDFs).
func ShouldTreatAsComic(libraryType string, formatGroup string) bool {
comicLibraries := map[string]bool{
"manga": true,
"comics": true,
}
fixedFormats := map[string]bool{
"fixed_layout": true,
"comic_archive": true,
}
return comicLibraries[libraryType] && fixedFormats[formatGroup]
}
// shouldEnablePanelDetection determines if panel detection should be enabled
// based on library type and format group
func shouldEnablePanelDetection(libraryType string, formatGroup string) bool {
+7
View File
@@ -17,6 +17,7 @@ import (
"bookhoard/internal/handlers"
"bookhoard/internal/services"
"bookhoard/internal/utils"
"bookhoard/internal/version"
"bookhoard/templates"
"github.com/golang-jwt/jwt/v5"
@@ -873,6 +874,11 @@ func registerFrontendRoutes(cfg *Config) {
FilePath: row.FilePath,
MissingScans: row.MissingScanCount,
}
if row.MatchID.Valid {
item.MatchID = uuid.UUID(row.MatchID.Bytes).String()
item.MatchTitle = row.MatchTitle
item.MatchLibraryName = getText(row.MatchLibraryName)
}
if row.ArchivedAt.Valid {
archived := row.ArchivedAt.Time
item.ArchivedAt = &archived
@@ -1450,6 +1456,7 @@ func (cfg *Config) GetHealth(c *echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"status": "healthy",
"version": version.Version,
"database": "connected",
"scan": map[string]interface{}{
"scan_in_progress": cfg.Worker.HasActiveScans(),
+1
View File
@@ -64,6 +64,7 @@ func registerMediaRoutes(cfg *Config) {
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
admin.POST("/media-items/purge-archived", cfg.MediaHandler.PurgeArchivedMediaItems)
admin.POST("/media-items/:id/unarchive", cfg.MediaHandler.UnarchiveMediaItem)
admin.POST("/media-items/:id/merge", cfg.MediaHandler.MergeArchivedMediaItem)
// Shelf management (protected)
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
+1
View File
@@ -19,6 +19,7 @@ func registerOPDSRoutes(cfg *Config) {
opds := e.Group("/opds/devices")
opds.Use(cfg.DeviceAuthMiddleware.Authenticate)
opds.GET("/:deviceId/catalog", cfg.OPDSHandler.GetDeviceCatalog)
opds.GET("/:deviceId/library/:libraryId/catalog", cfg.OPDSHandler.GetLibraryCatalog)
opds.GET("/:deviceId/search", cfg.OPDSHandler.SearchDeviceCatalog)
opds.GET("/:deviceId/nav", cfg.OPDSHandler.GetDeviceNavigation)
opds.GET("/:deviceId/download/:bookId", cfg.OPDSHandler.DownloadBook)
+8
View File
@@ -66,6 +66,13 @@ func registerReaderRoutes(cfg *Config) {
if !visible {
return renderErrorPage(c, "Access denied", "access_denied")
}
// Library type decides comic reader treatment: fixed-layout items
// (e.g. manga PDFs) shelved in comics/manga libraries are paged
// through like comics — bookmarks only, no highlights or search.
libraryWithType, err := cfg.Queries.GetLibraryWithType(c.Request().Context(), mediaItem.LibraryID)
if err != nil {
return renderErrorPage(c, "Failed to get library info", "library_error")
}
// Convert to template types. Reading state is deliberately NOT
// fetched or embedded: the reader pulls position, bookmarks, and
// annotations from the APIs at open time so the page can never
@@ -89,6 +96,7 @@ func registerReaderRoutes(cfg *Config) {
FileURL: utils.ResolveMediaURL(mediaItem.LibraryID, pgtype.Text{String: mediaItem.FilePath, Valid: true}),
TotalCharacters: mediaItem.TotalCharacters.Int64,
EstimatedPages: sync.EstimatedPages(mediaItem.TotalCharacters.Int64),
TreatAsComic: handlers.ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup),
}
// Render template
var buf bytes.Buffer
+141
View File
@@ -848,6 +848,17 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
}
}
// Cross-library move detection: the file may have been moved into a
// different library of the same type (e.g. reorganizing ebooks into a
// new library). If identical content is registered elsewhere and the old
// file is gone from disk, repoint that row so reading history follows
// the book instead of creating a duplicate.
if hashInfo.FileSHA256 != "" {
if handled := s.detectCrossLibraryMove(ctx, hashInfo.FileSHA256, libraryID, path, info.Size()); handled {
return false, nil
}
}
// REMOVED: Comic metadata extraction now handled by mergeMetadata()
// This avoids duplicate extraction and ensures smart merging happens
@@ -1741,6 +1752,136 @@ func (s *MediaScanner) DetectFixedLayoutEPUB(epubPath string) (bool, error) {
return false, nil
}
// moveCandidate is the subset of a cross-library hash-match row the scanner
// needs to decide whether a file is a move or a deliberate copy.
type moveCandidate struct {
ID pgtype.UUID
LibraryID pgtype.UUID
FilePath string
Title string
Archived bool
MissingScans int32
}
// selectMoveCandidate returns the first candidate whose file has vanished
// from its library folders. Candidates arrive pre-ordered (archived first,
// then most missing scans, then oldest), so the first missing one is the
// best repoint target. Candidates whose file still exists are deliberate
// copies in another library and are left untouched. Returns ok=false when
// every candidate is still on disk.
func selectMoveCandidate(candidates []moveCandidate, fileExists func(moveCandidate) bool) (moveCandidate, bool) {
for _, candidate := range candidates {
if !fileExists(candidate) {
return candidate, true
}
}
return moveCandidate{}, false
}
// detectCrossLibraryMove repoints an existing row from another same-type
// library when this file is that row's content at a new location and the old
// file is gone from disk. Returns true when the file was fully handled as a
// cross-library move (or rejected as type-incompatible) and must not be
// imported again by the caller.
func (s *MediaScanner) detectCrossLibraryMove(ctx context.Context, sha string, libraryID pgtype.UUID, path string, size int64) bool {
rows, err := s.db.GetMediaItemsBySHA256AndSameLibraryType(ctx, database.GetMediaItemsBySHA256AndSameLibraryTypeParams{
FileSha256: pgtype.Text{String: sha, Valid: true},
LibraryID: libraryID,
})
if err != nil {
if !errors.Is(err, pgx.ErrNoRows) {
fmt.Printf("Warning: cross-library SHA lookup failed for %s: %v\n", path, err)
}
return false
}
if len(rows) == 0 {
return false
}
candidates := make([]moveCandidate, 0, len(rows))
for _, row := range rows {
candidates = append(candidates, moveCandidate{
ID: row.ID,
LibraryID: row.LibraryID,
FilePath: row.FilePath,
Title: row.Title,
Archived: row.ArchivedAt.Valid,
MissingScans: row.MissingScanCount,
})
}
candidate, ok := selectMoveCandidate(candidates, func(c moveCandidate) bool {
return s.mediaItemFileExists(ctx, c.LibraryID, c.FilePath)
})
if !ok {
// Every registered copy of this content still lives in its own
// library: a deliberate multi-library import, not a move.
return false
}
return s.repointCrossLibraryMove(ctx, candidate, libraryID, path, size)
}
// mediaItemFileExists resolves a media item's relative path against its own
// library's folders and reports whether the file is present on disk.
func (s *MediaScanner) mediaItemFileExists(ctx context.Context, libraryID pgtype.UUID, relativePath string) bool {
folders, err := s.db.GetLibraryFolders(ctx, libraryID)
if err != nil {
return false
}
for _, folder := range folders {
if _, statErr := os.Stat(filepath.Join(folder.FolderPath, relativePath)); statErr == nil {
return true
}
}
return false
}
// repointCrossLibraryMove validates the target library's type rules, then
// moves the candidate row into the target library at its new path. Reading
// history, annotations and collections follow automatically because the row
// keeps its ID. Returns true when the file was handled (moved or rejected)
// and must not be imported again.
func (s *MediaScanner) repointCrossLibraryMove(ctx context.Context, candidate moveCandidate, libraryID pgtype.UUID, path string, size int64) bool {
item, err := s.db.GetMediaItem(ctx, candidate.ID)
if err != nil {
fmt.Printf("Warning: failed to load cross-library move candidate %q: %v\n", candidate.Title, err)
return false
}
targetLib, err := s.db.GetLibraryWithType(ctx, libraryID)
if err != nil {
fmt.Printf("Warning: failed to load target library for cross-library move of %q: %v\n", candidate.Title, err)
return false
}
// Respect the target library's type rules: a reflowable EPUB must not
// silently land in a manga library, mirroring import validation.
if issue := s.ValidateMediaItemForLibrary(ctx, item, targetLib); issue != nil {
fmt.Printf("[MOVE] Rejected cross-library move of %q: %s\n", candidate.Title, *issue)
s.logger.LogDelete(fmt.Sprintf("[MOVE] Rejected moving item '%s' into library %s: %s", candidate.Title, libraryID, *issue))
if logErr := s.LogProcessingIssue(ctx, uuid.UUID(candidate.ID.Bytes), uuid.UUID(libraryID.Bytes),
"library_type_mismatch", *issue, "warning"); logErr != nil {
fmt.Printf("Warning: failed to record processing issue for %q: %v\n", candidate.Title, logErr)
}
return true
}
if err := s.db.MoveMediaItemToLibrary(ctx, database.MoveMediaItemToLibraryParams{
ID: candidate.ID,
LibraryID: libraryID,
FilePath: s.getRelativePath(path),
FileSize: pgtype.Int8{Int64: size, Valid: true},
}); err != nil {
fmt.Printf("Warning: failed to repoint cross-library move %q -> %s: %v\n", candidate.FilePath, path, err)
return false
}
fmt.Printf("[MOVE] Cross-library move: %q -> %s (history preserved)\n", candidate.FilePath, path)
s.logger.LogDelete(fmt.Sprintf("[MOVE] Repointed item '%s' from %q to %s (library changed)", candidate.Title, candidate.FilePath, path))
return true
}
// ValidateMediaItemForLibrary checks if item matches library type expectations
// and returns issue description if validation fails, nil if valid
func (s *MediaScanner) ValidateMediaItemForLibrary(
@@ -0,0 +1,67 @@
package services
import (
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
func mustUUID(t *testing.T, raw string) pgtype.UUID {
t.Helper()
parsed, err := uuid.Parse(raw)
if err != nil {
t.Fatalf("invalid test uuid %q: %v", raw, err)
}
return pgtype.UUID{Bytes: parsed, Valid: true}
}
// alwaysExists simulates every candidate's file still being on disk:
// deliberate copies across libraries must never be repointed.
func TestSelectMoveCandidateAllFilesExist(t *testing.T) {
candidates := []moveCandidate{
{ID: mustUUID(t, "00000000-0000-0000-0000-000000000001"), FilePath: "a/book.epub"},
{ID: mustUUID(t, "00000000-0000-0000-0000-000000000002"), FilePath: "b/book.epub"},
}
if _, ok := selectMoveCandidate(candidates, func(moveCandidate) bool { return true }); ok {
t.Fatal("expected no move candidate when every file still exists")
}
}
// Input arrives pre-ordered (archived first, then most missing scans, then
// oldest), so the first missing candidate must win.
func TestSelectMoveCandidatePicksFirstMissing(t *testing.T) {
onDisk := moveCandidate{ID: mustUUID(t, "00000000-0000-0000-0000-000000000001"), FilePath: "a/book.epub"}
gone := moveCandidate{ID: mustUUID(t, "00000000-0000-0000-0000-000000000002"), FilePath: "b/book.epub"}
candidates := []moveCandidate{onDisk, gone}
got, ok := selectMoveCandidate(candidates, func(c moveCandidate) bool {
return c.ID == onDisk.ID
})
if !ok {
t.Fatal("expected a move candidate")
}
if got.ID != gone.ID {
t.Fatalf("expected candidate %v, got %v", gone.ID, got.ID)
}
}
func TestSelectMoveCandidateOnlyMissingQualifies(t *testing.T) {
gone := moveCandidate{ID: mustUUID(t, "00000000-0000-0000-0000-000000000001"), FilePath: "old/book.epub"}
candidates := []moveCandidate{gone}
got, ok := selectMoveCandidate(candidates, func(moveCandidate) bool { return false })
if !ok {
t.Fatal("expected the missing candidate to qualify")
}
if got.ID != gone.ID {
t.Fatalf("expected candidate %v, got %v", gone.ID, got.ID)
}
}
func TestSelectMoveCandidateEmptyList(t *testing.T) {
if _, ok := selectMoveCandidate(nil, func(moveCandidate) bool { return false }); ok {
t.Fatal("expected no candidate for an empty list")
}
}
+115 -24
View File
@@ -431,7 +431,9 @@ func firstTextDescendant(n *html.Node) *html.Node {
// textNodeAtRuneOffset walks text nodes under elem in document order and
// returns the node containing the rune offset plus the local offset within
// that node. Offsets beyond the end clamp to the last node.
func textNodeAtRuneOffset(elem *html.Node, offset int) (*html.Node, int) {
// textNodeAtUTF16Offset resolves a crengine text().N offset — UTF-16 code
// units — to (text node, rune offset) within elem's text.
func textNodeAtUTF16Offset(elem *html.Node, offset int) (*html.Node, int) {
if offset < 0 {
offset = 0
}
@@ -441,15 +443,15 @@ func textNodeAtRuneOffset(elem *html.Node, offset int) (*html.Node, int) {
var walk func(*html.Node) bool
walk = func(node *html.Node) bool {
if node.Type == html.TextNode {
length := utf8.RuneCountInString(node.Data)
length := utf16Len(node.Data)
if remaining < length {
target = node
local = remaining
local = utf16ToRuneIndex(node.Data, remaining)
return true
}
remaining -= length
target = node
local = length
local = utf8.RuneCountInString(node.Data)
return false
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
@@ -507,7 +509,7 @@ func (c *CFIConverter) convertByStructuralPath(body *html.Node, xp *CREXPointer,
var textNode *html.Node
var localOffset int
if xp.CharOffset > 0 {
textNode, localOffset = textNodeAtRuneOffset(elem, xp.CharOffset)
textNode, localOffset = textNodeAtUTF16Offset(elem, xp.CharOffset)
} else {
textNode = firstTextDescendant(elem)
localOffset = 0
@@ -1058,24 +1060,96 @@ func indexChildNodes(parent *html.Node) []indexedNode {
return nodes
}
func findTextChunkIndex(parent *html.Node, textNode *html.Node) (int, int) {
// Character-offset currency policy: the EPUB CFI spec and crengine both
// count UTF-16 code units (JavaScript `.length` semantics — what foliate,
// readium, KOReader and Kobo clients all observe), so every offset that
// CROSSES the wire — CFI terminals, CRE text() offsets, the served
// char_offset handle — is UTF-16. Internal arithmetic (book-level
// character_offset, percentage fractions) stays rune-based, consistent
// with TotalCharacters. These helpers convert at the boundaries; for
// all-BMP text the two currencies are identical, so ASCII books are
// unaffected.
// utf16Len returns the UTF-16 code-unit length of s.
func utf16Len(s string) int {
n := 0
for _, r := range s {
if r >= 0x10000 {
n += 2
} else {
n++
}
}
return n
}
// utf16ToRuneIndex converts a UTF-16 code-unit offset within s to a rune
// index (clamped to len(runes)).
func utf16ToRuneIndex(s string, u16 int) int {
if u16 <= 0 {
return 0
}
units := 0
i := 0
for _, r := range s {
if units >= u16 {
return i
}
if r >= 0x10000 {
units += 2
} else {
units++
}
i++
}
return i
}
// runeToUTF16Index converts a rune index within s to a UTF-16 code-unit
// offset (clamped to the string's unit length).
func runeToUTF16Index(s string, runeIdx int) int {
if runeIdx <= 0 {
return 0
}
units := 0
i := 0
for _, r := range s {
if i >= runeIdx {
return units
}
if r >= 0x10000 {
units += 2
} else {
units++
}
i++
}
return units
}
// findTextChunk locates the indexed text chunk containing textNode and
// returns its chunk index, the rune offset of the node within the chunk,
// and the chunk text up to and including the node (for UTF-16 conversion
// of chunk-relative offsets).
func findTextChunk(parent *html.Node, textNode *html.Node) (int, int, string) {
indexed := indexChildNodes(parent)
for i, node := range indexed {
if node.isTextChunk() {
for j, tn := range node.textChunk {
var sb strings.Builder
chunkOffset := 0
for _, tn := range node.textChunk {
if tn == textNode {
chunkOffset := 0
for k := 0; k < j; k++ {
chunkOffset += utf8.RuneCountInString(node.textChunk[k].Data)
}
return i, chunkOffset
return i, chunkOffset, sb.String() + textNode.Data
}
chunkOffset += utf8.RuneCountInString(tn.Data)
sb.WriteString(tn.Data)
}
}
}
return -1, 0
return -1, 0, ""
}
func findElementCFIIndex(parent *html.Node, element *html.Node) int {
indexed := indexChildNodes(parent)
for i, node := range indexed {
@@ -1110,15 +1184,17 @@ func buildCFI(spineIndex int, textNode *html.Node, charOffset int) (string, erro
return "", fmt.Errorf("text node has no parent")
}
chunkIdx, chunkOffset := findTextChunkIndex(parent, textNode)
chunkIdx, chunkOffset, chunkText := findTextChunk(parent, textNode)
if chunkIdx == -1 {
return "", fmt.Errorf("text node not found in parent's indexed children")
}
// The CFI terminal offset is UTF-16 code units (spec currency); the
// internal charOffset is runes. Convert over the chunk text.
totalOffset := chunkOffset + charOffset
var parts []string
parts = append(parts, fmt.Sprintf("/%d:%d", chunkIdx, totalOffset))
parts = append(parts, fmt.Sprintf("/%d:%d", chunkIdx, runeToUTF16Index(chunkText, totalOffset)))
current := parent
for current != nil {
@@ -1245,9 +1321,16 @@ func countTextChars(n *html.Node) int {
return count
}
// countTextCharsBefore counts the text characters preceding target within
// its document: preceding siblings at each ancestor level, strictly
// EXCLUDING target's own subtree (callers add the in-node offset
// separately). Starting the walk at target itself — or recursing after
// counting the current node's subtree — would re-count the accumulated
// document once per ancestor level and multiply the result by the node's
// depth.
func countTextCharsBefore(target *html.Node) int {
count := 0
for c := target; c != nil; c = c.PrevSibling {
for c := target.PrevSibling; c != nil; c = c.PrevSibling {
count += countTextChars(c)
}
if target.Parent != nil {
@@ -1519,24 +1602,31 @@ func resolveCFIToNode(doc *html.Node, steps []cfiStep) (*html.Node, int, error)
entry := indexed[lastStep.Index]
if entry.isTextChunk() {
textOffset := 0
// The CFI terminal offset arrives in UTF-16 code units (spec
// currency — foliate/readium/KOReader/Kobo all emit UTF-16).
// Walk the chunk in UTF-16 units, then convert the hit position
// to the internal rune offset.
u16Remaining := 0
if lastStep.HasOffset {
textOffset = lastStep.Offset
u16Remaining = lastStep.Offset
}
var targetNode *html.Node
remainingOffset := textOffset
runeIntoTarget := 0
for _, tn := range entry.textChunk {
textLen := utf8.RuneCountInString(tn.Data)
if remainingOffset < textLen || (remainingOffset == textLen && targetNode == nil) {
units := utf16Len(tn.Data)
if u16Remaining < units || (u16Remaining == units && targetNode == nil) {
targetNode = tn
runeIntoTarget = utf16ToRuneIndex(tn.Data, u16Remaining)
break
}
remainingOffset -= textLen
u16Remaining -= units
targetNode = tn
runeIntoTarget = utf8.RuneCountInString(tn.Data)
}
if targetNode == nil && len(entry.textChunk) > 0 {
targetNode = entry.textChunk[len(entry.textChunk)-1]
runeIntoTarget = utf8.RuneCountInString(targetNode.Data)
}
parent := targetNode.Parent
@@ -1547,7 +1637,7 @@ func resolveCFIToNode(doc *html.Node, steps []cfiStep) (*html.Node, int, error)
}
totalOffset += countTextChars(c)
}
totalOffset += remainingOffset
totalOffset += runeIntoTarget
return targetNode, totalOffset, nil
}
@@ -1606,7 +1696,8 @@ func buildCREXPointer(spineIndex int, node *html.Node, charOffset int) (string,
xpointer := fmt.Sprintf("/body/DocFragment[%d]/body%s", fragIndex, strings.Join(parts, ""))
if charOffset > 0 || (node.Type == html.TextNode) {
xpointer += fmt.Sprintf("/text().%d", charOffset)
// crengine counts UTF-16 code units; charOffset is internal runes.
xpointer += fmt.Sprintf("/text().%d", runeToUTF16Index(node.Data, charOffset))
}
return xpointer, nil
+1 -1
View File
@@ -133,7 +133,7 @@ func writeTestEPUB(t *testing.T) string {
}
docs := []spineDoc{
{"doc1.xhtml", "<body><div><p>Chapter one opening page.</p></div></body>"},
{"doc2.xhtml", "<body><div><p>The family of Dashwood had long been settled in Sussex.</p><p>Their estate was large, and their residence was at Norland Park.</p></div></body>"},
{"doc2.xhtml", "<body><div><p>The family of Dashwood had long been settled in Sussex.</p><p>Their estate was large, and their residence was at Norland Park.</p><p>The family crest shows a globe \U0001F30D and a rocket \U0001F680 flying onward.</p></div></body>"},
{"doc3.xhtml", "<body><div><p>Chapter three contents.</p></div></body>"},
{"doc4.xhtml", "<body><div><p>Chapter four contents.</p></div></body>"},
{"doc5.xhtml", "<body><div><p>Chapter five contents.</p></div></body>"},
+349
View File
@@ -0,0 +1,349 @@
package sync
// Ingest-side position authority for reading progress.
//
// Clients submit (percentage, context_text, epubcfi). The epubcfi is a
// standard wrapped CFI — epubcfi(/6/N!/…) — resolvable against the same
// document this package parses, so every submission is INDEPENDENTLY
// verified: the text at the resolved anchor is extracted and compared
// with the submitted context. A mismatch (or an unresolvable anchor)
// heals the position by text search, with the submitted percentage
// disambiguating repeated phrases, instead of trusting a client-side
// projection. This keeps buggy clients from poisoning stored positions:
// a resolver that silently returns "wherever I'm currently scrolled"
// fails the context check and gets healed to the true location.
//
// The anchor's block element is also derived as a cssSelector plus a
// block-relative character offset and served back — the readium-native
// handle clients scroll to, so they never have to parse or trust CFIs
// themselves.
//
// Note: readium-based clients number their readingOrder excluding
// linear="no" spine items, while this package's spine index follows the
// OPF spine as written. The spine index is therefore internal-only;
// client-facing responses carry the anchor document's href instead.
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/net/html"
)
// IsConvertibleFormat reports whether a format group uses standard CFIs
// as its structural locator currency (i.e. whether CFI verification and
// cssSelector derivation apply to it).
func IsConvertibleFormat(formatGroup string) bool {
return isConvertible(string(formatGroup))
}
func truncateRunes(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n])
}
// anchorBlock returns the nearest non-inline (block-level) ancestor of a
// resolved text/element node — the server-side equivalent of the
// readers' computed-style block walk.
func anchorBlock(node *html.Node) *html.Node {
if node == nil {
return nil
}
if node.Type == html.ElementNode && !isInlineFormatting(node) {
return node
}
return findBlockParent(node)
}
// blockContextText extracts the normalized text from (node, runeOff) to
// the end of the anchor block — the same excerpt rule the readers use for
// context_text, ≤100 chars.
func blockContextText(block, node *html.Node, runeOff int) string {
segments := collectInlineText(block)
var sb strings.Builder
started := false
for _, seg := range segments {
if !started && seg.node == node {
started = true
runes := []rune(string(seg.runes))
if runeOff < len(runes) {
sb.WriteString(string(runes[runeOff:]))
}
continue
}
if started {
sb.WriteString(string(seg.runes))
}
}
return truncateRunes(normalizeWhitespace(sb.String()), 100)
}
// contextMatches reports whether a server-extracted context and a client-
// submitted context describe the same anchor. Both are suffixes of the
// same block text when the anchors share a block, so containment in
// either direction verifies; empty or very short contexts never match.
func contextMatches(serverCtx, submitted string) bool {
s := truncateRunes(normalizeWhitespace(submitted), 100)
t := truncateRunes(normalizeWhitespace(serverCtx), 100)
if s == "" || t == "" {
return false
}
short, long := s, t
if len([]rune(short)) > len([]rune(long)) {
short, long = long, short
}
if len([]rune(short)) < 12 {
return false
}
return strings.Contains(long, short)
}
// cssSelectorFor mirrors the readers' selOf: a body-relative
// tag:nth-child(k) chain (k = 1-based position among element siblings).
func cssSelectorFor(block *html.Node) string {
var segs []string
n := block
for n != nil && n.Type == html.ElementNode && n.Data != "body" {
k := 1
sib := n.Parent.FirstChild
for sib != nil && sib != n {
if sib.Type == html.ElementNode {
k++
}
sib = sib.NextSibling
}
segs = append([]string{n.Data + ":nth-child(" + fmt.Sprintf("%d", k) + ")"}, segs...)
n = n.Parent
}
return "body>" + strings.Join(segs, ">")
}
// blockCharOffset computes the offset of (node, runeOff) within the
// concatenated text of its block, in UTF-16 code units — the client-side
// scroll-target currency (JavaScript .length semantics).
func blockCharOffset(block, node *html.Node, runeOff int) int {
segments := collectInlineText(block)
var sb strings.Builder
for _, seg := range segments {
if seg.node == node {
runes := seg.runes
if runeOff < len(runes) {
runes = runes[:runeOff]
}
sb.WriteString(string(runes))
return runeToUTF16Index(sb.String(), utf8.RuneCountInString(sb.String()))
}
sb.WriteString(string(seg.runes))
}
return utf16Len(sb.String())
}
// bookCharOffset computes the book-wide rune offset of (node, runeOff) —
// the reading_progress.character_offset column's currency, consistent with
// TotalCharacters and the percentage derivations.
func bookCharOffset(conv *CFIConverter, spineIndex int, node *html.Node, runeOff int) int {
spine, err := conv.loadSpine()
if err != nil {
return 0
}
before := 0
for i := 0; i < spineIndex && i < len(spine.items); i++ {
doc, _, derr := conv.getContentDoc(i + 1)
if derr != nil {
continue
}
if b := findBody(doc); b != nil {
before += countTextChars(b)
}
}
return before + countTextCharsBefore(node) + runeOff
}
// ProgressAnchor is the full server-computed apply handle for a stored
// standard CFI: the anchor block's cssSelector, the character offset
// within that block's text, and the spine document's href.
type ProgressAnchor struct {
CSSSelector string
CharOffset int
Href string
HealedCFI string
Healed bool
HealedPct *float64
}
// VerifyProgressAnchor resolves a client-submitted standard CFI against
// the EPUB, cross-checks the submitted context text, and heals the anchor
// by text search on any mismatch. charOffset is the anchor's block-
// relative UTF-16 offset (the served char_offset handle); bookOffset is
// the anchor's book-wide rune offset (the character_offset column's
// currency) — callers refresh the column from it on every verified save
// so it never goes stale behind the anchor.
func VerifyProgressAnchor(epubPath, epubcfi, contextText string, percentage float64) (finalCFI string, cssSelector string, anchorHref string, charOffset *int, bookOffset *int, healedPct *float64, healed bool, err error) {
finalCFI = epubcfi
anchorHref = ""
spineIndex, localSteps, err := parseEPUBCFI(epubcfi)
if err != nil {
cfi, sel, href, off, book, pct, healedFlag, herr := healFromContext(epubPath, contextText, percentage)
return cfi, sel, href, off, book, pct, healedFlag, herr
}
conv := cachedConverter(epubPath)
doc, docHref, err := conv.getContentDoc(spineIndex + 1)
if err != nil {
return healFromContext(epubPath, contextText, percentage)
}
node, runeOff, rerr := resolveCFIToNode(doc, localSteps)
if rerr != nil {
return healFromContext(epubPath, contextText, percentage)
}
anchorHref = docHref
block := anchorBlock(node)
serverCtx := blockContextText(block, node, runeOff)
if contextMatches(serverCtx, contextText) {
off := blockCharOffset(block, node, runeOff)
book := bookCharOffset(conv, spineIndex, node, runeOff)
return finalCFI, cssSelectorFor(block), anchorHref, &off, &book, nil, false, nil
}
// Mismatch: heal by text search.
hCFI, hPct, hSel, hHref, hBook, herr := healAnchorByText(epubPath, contextText, percentage, spineIndex)
if herr != nil {
return finalCFI, "", hHref, nil, nil, nil, false, fmt.Errorf("context mismatch (server %q vs client %q) and heal failed: %w",
truncateRunes(serverCtx, 40), truncateRunes(contextText, 40), herr)
}
hOff := blockCharOffsetFor(epubPath, hCFI)
return hCFI, hSel, hHref, &hOff, &hBook, &hPct, true, nil
}
func healFromContext(epubPath, contextText string, percentage float64) (string, string, string, *int, *int, *float64, bool, error) {
cfi, healedPct, sel, href, book, err := healAnchorByText(epubPath, contextText, percentage, -1)
if err != nil {
return "", "", "", nil, nil, nil, false, err
}
off := blockCharOffsetFor(epubPath, cfi)
return cfi, sel, href, &off, &book, &healedPct, true, nil
}
func healAnchorByText(epubPath, contextText string, percentage float64, spineIndex int) (string, float64, string, string, int, error) {
if epubPath == "" {
return "", 0, "", "", 0, fmt.Errorf("no epub available for text anchoring")
}
conv := cachedConverter(epubPath)
spine, err := conv.loadSpine()
if err != nil {
return "", 0, "", "", 0, err
}
needle := truncateRunes(normalizeWhitespace(contextText), 40)
if len([]rune(needle)) < 12 {
return "", 0, "", "", 0, fmt.Errorf("context too short to anchor")
}
type match struct {
spine int
node *html.Node
off int
}
var matches []match
totalChars := 0
charsBefore := make([]int, len(spine.items))
for i := range spine.items {
doc, _, derr := conv.getContentDoc(i + 1)
if derr != nil {
continue
}
body := findBody(doc)
if body == nil {
continue
}
charsBefore[i] = totalChars
totalChars += countTextChars(body)
if n, runeOff := findTextInNode(body, needle); n != nil {
matches = append(matches, match{spine: i, node: n, off: runeOff})
}
}
if len(matches) == 0 || totalChars <= 0 {
return "", 0, "", "", 0, fmt.Errorf("context not found in book")
}
best := matches[0]
bestDist := -1.0
for _, m := range matches {
frac := (float64(charsBefore[m.spine]) + float64(countTextCharsBefore(m.node)+m.off)) / float64(totalChars)
d := frac - percentage
if d < 0 {
d = -d
}
if bestDist < 0 || d < bestDist {
best = m
bestDist = d
}
}
bookOff := charsBefore[best.spine] + countTextCharsBefore(best.node) + best.off
healedPct := float64(bookOff) / float64(totalChars)
cfi, err := buildCFI(best.spine, best.node, best.off)
if err != nil {
return "", 0, "", "", 0, err
}
sel := ""
if block := anchorBlock(best.node); block != nil {
sel = cssSelectorFor(block)
}
return cfi, healedPct, sel, spine.items[best.spine].href, bookOff, nil
}
func blockCharOffsetFor(epubPath, cfi string) int {
n, off, _, herr := cfiTextAtAnchorInternal(epubPath, cfi)
if herr != nil {
return 0
}
block := anchorBlock(n)
if block == nil {
return 0
}
return blockCharOffset(block, n, off)
}
// cfiTextAtAnchorInternal resolves a stored standard CFI to its anchor
// text node, rune offset, and containing document.
func cfiTextAtAnchorInternal(epubPath, cfi string) (*html.Node, int, *html.Node, error) {
spineIndex, localSteps, err := parseEPUBCFI(cfi)
if err != nil {
return nil, 0, nil, err
}
conv := cachedConverter(epubPath)
doc, _, err := conv.getContentDoc(spineIndex + 1)
if err != nil {
return nil, 0, nil, err
}
node, off, err := resolveCFIToNode(doc, localSteps)
return node, off, doc, err
}
// ProgressCSSSelector derives the anchor block's cssSelector from a stored
// standard CFI.
func ProgressCSSSelector(epubPath, epubcfi string) (string, error) {
spineIndex, localSteps, err := parseEPUBCFI(epubcfi)
if err != nil {
return "", err
}
conv := cachedConverter(epubPath)
doc, _, err := conv.getContentDoc(spineIndex + 1)
if err != nil {
return "", err
}
node, _, err := resolveCFIToNode(doc, localSteps)
if err != nil {
return "", err
}
block := anchorBlock(node)
if block == nil {
return "", fmt.Errorf("no block ancestor for anchor")
}
return cssSelectorFor(block), nil
}
+22
View File
@@ -0,0 +1,22 @@
package sync
import (
"os"
"testing"
)
func TestManualRealBookAnchor(t *testing.T) {
path := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (1269)/1984 - George Orwell.epub"
if _, err := os.Stat(path); err != nil {
t.Skip("real book not present on this machine")
}
cfi := "epubcfi(/6/2!/4[x1984]/8[_idContainer003]/62/1:456)"
ctx := "was at war with one of these Powers it was generally at peace with the other"
finalCFI, sel, href, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, cfi, ctx, 0.0415)
if err != nil {
t.Fatalf("VerifyProgressAnchor error: %v", err)
}
t.Logf("final=%q\n healed=%v healedPct=%v\n selector=%q href=%q charOff=%v", finalCFI, healed, healedPct, sel, href, charOff)
sel2, err2 := ProgressCSSSelector(path, cfi)
t.Logf("ProgressCSSSelector: %q err=%v", sel2, err2)
}
+248
View File
@@ -0,0 +1,248 @@
package sync
import (
"fmt"
"testing"
)
// The fixture (writeTestEPUB in cfi_converter_test.go): 6 spine docs.
// doc2 = spine index 1 with two paragraphs:
// p1: "The family of Dashwood had long been settled in Sussex."
// p2: "Their estate was large, and their residence was at Norland Park."
// Hand-derived local paths (html→body /4, body→div /4, div→p /4|/6,
// p→text chunk /1):
const (
dashwoodCFI = "epubcfi(/6/4!/4/2/2/1:0)"
dashwoodText = "The family of Dashwood had long been settled in Sussex."
estateCFI = "epubcfi(/6/4!/4/2/4/1:0)"
estateText = "Their estate was large, and their residence was at Norland Park."
dashwoodSelect = "body>div:nth-child(1)>p:nth-child(1)"
estateSelect = "body>div:nth-child(1)>p:nth-child(2)"
)
func TestVerifyProgressAnchorAcceptsExact(t *testing.T) {
path := writeTestEPUB(t)
finalCFI, sel, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, dashwoodCFI, dashwoodText, 0.3)
_ = charOff
if err != nil {
t.Fatalf("verify error: %v", err)
}
if healed {
t.Errorf("exact anchor should not heal")
}
if healedPct != nil {
t.Errorf("exact anchor should not carry a healed percentage")
}
if finalCFI != dashwoodCFI {
t.Errorf("finalCFI = %q, want unchanged %q", finalCFI, dashwoodCFI)
}
if sel != dashwoodSelect {
t.Errorf("cssSelector = %q, want %q", sel, dashwoodSelect)
}
}
func TestVerifyProgressAnchorHealsMismatch(t *testing.T) {
path := writeTestEPUB(t)
// Anchored at p1 but the context is p2's text: the classic
// client-projection bug — the server must heal to the true location.
finalCFI, _, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, dashwoodCFI, estateText, 0.3)
_ = charOff
if err != nil {
t.Fatalf("verify error: %v", err)
}
if !healed {
t.Fatalf("expected healing, got none (cfi=%q)", finalCFI)
}
if finalCFI != estateCFI {
t.Errorf("healed CFI = %q, want %q", finalCFI, estateCFI)
}
if healedPct == nil || *healedPct <= 0 {
t.Errorf("healed percentage not recomputed: %v", healedPct)
}
// Healing must converge: re-verifying the healed anchor is a no-op.
finalCFI2, _, _, _, _, healedPct2, healed2, err := VerifyProgressAnchor(path, finalCFI, estateText, 0.3)
if err != nil {
t.Fatalf("re-verify error: %v", err)
}
if healed2 {
t.Errorf("healed anchor should be stable, got healed again to %q (pct %v)", finalCFI2, healedPct2)
}
if finalCFI2 != finalCFI {
t.Errorf("re-verify CFI = %q, want %q", finalCFI2, finalCFI)
}
}
func TestVerifyProgressAnchorHealsUnresolvable(t *testing.T) {
path := writeTestEPUB(t)
// Element index 99 is out of range in doc2 — the anchor cannot resolve.
bogus := "epubcfi(/6/4!/4/2/99/1:0)"
finalCFI, _, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, bogus, dashwoodText, 0.05)
_ = charOff
if err != nil {
t.Fatalf("verify error: %v", err)
}
if !healed {
t.Fatal("unresolvable anchor should heal from context")
}
if finalCFI != dashwoodCFI {
t.Errorf("healed CFI = %q, want the Dashwood anchor %q", finalCFI, dashwoodCFI)
}
if healedPct == nil {
t.Errorf("healed percentage not recomputed")
}
}
func TestVerifyProgressAnchorDumbClient(t *testing.T) {
path := writeTestEPUB(t)
// No CFI at all — a client that only knows percentage + context is
// fully supported: the server anchors structurally from the text.
finalCFI, sel, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, "", estateText, 0.3)
_ = charOff
if err != nil {
t.Fatalf("verify error: %v", err)
}
if !healed {
t.Fatal("context-only submission should count as anchored-by-heal")
}
if finalCFI != estateCFI {
t.Errorf("anchored CFI = %q, want %q", finalCFI, estateCFI)
}
if sel != estateSelect {
t.Errorf("cssSelector = %q, want %q", sel, estateSelect)
}
if healedPct == nil {
t.Errorf("percentage not recomputed for context-only anchor")
}
}
func TestContextMatches(t *testing.T) {
block := "The family of Dashwood had long been settled in Sussex."
cases := []struct {
name string
serverCtx string
submitted string
want bool
}{
{"exact", block, block, true},
{"suffix of block", block, "settled in Sussex.", true},
{"block is suffix", "settled in Sussex.", block, true},
{"mid-block substring", block, "Dashwood had long been", true},
{"different text", block, "completely unrelated words here", false},
{"too short", block, "the", false},
{"empty submitted", block, "", false},
{"empty server", "", "some long enough context text", false},
}
for _, tc := range cases {
if got := contextMatches(tc.serverCtx, tc.submitted); got != tc.want {
t.Errorf("contextMatches(%q, %q) = %v, want %v", tc.serverCtx, tc.submitted, got, tc.want)
}
}
}
func TestParseStandardCFIRange(t *testing.T) {
// Web progress CFIs are range CFIs; parse must resolve to the start arm.
spineIndex, localSteps, err := parseEPUBCFI("epubcfi(/6/4!/4/4:0,/4/4:53)")
if err != nil {
t.Fatalf("parse error: %v", err)
}
if spineIndex != 1 {
t.Errorf("spineIndex = %d, want 1", spineIndex)
}
if len(localSteps) != 4 {
t.Fatalf("steps = %d, want 4 (parent + start arm)", len(localSteps))
}
last := localSteps[len(localSteps)-1]
if last.Index != 4 || !last.HasOffset || last.Offset != 53 {
t.Errorf("start-arm step = %+v, want /4:0", last)
}
}
// The emoji in fixture doc2's third paragraph are astral plane runes:
// one rune, two UTF-16 code units. Every wire offset (CFI terminals,
// the served char_offset) must therefore be UTF-16, while the book-wide
// character_offset column stays rune-based.
//
// Case 1: the context starts at rune 31 of the paragraph text ("The
// family crest shows a globe " = 31 BMP runes), so its UTF-16 offset is
// also 31 — the currencies agree.
//
// Case 2: the context starts at rune 33 ("and a rocket ..."), with the
// astral 🌍 (rune 31, units 31-32) BEFORE the offset — the UTF-16 offset
// is 34, one more than the rune offset. That +1 is the whole point of
// the boundary conversion.
//
// Local path: p3 of doc2's div = /4/2/6, text chunk /1.
func TestAstralOffsetCurrency(t *testing.T) {
path := writeTestEPUB(t)
cases := []struct {
name string
context string
wantTerm int
}{
{"before any emoji", "🌍 and a rocket 🚀 flying onward.", 31},
{"after one emoji", "and a rocket 🚀 flying onward.", 34},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
wantCFI := fmt.Sprintf("epubcfi(/6/4!/4/2/6/1:%d)", tc.wantTerm)
finalCFI, sel, _, charOff, bookOff, _, healed, err := VerifyProgressAnchor(path, "", tc.context, 0.3)
if err != nil {
t.Fatalf("heal error: %v", err)
}
if !healed {
t.Fatalf("expected heal from context-only submission")
}
if finalCFI != wantCFI {
t.Errorf("healed CFI = %q, want %q (UTF-16 terminal)", finalCFI, wantCFI)
}
if sel != "body>div:nth-child(1)>p:nth-child(3)" {
t.Errorf("cssSelector = %q", sel)
}
if charOff == nil || *charOff != tc.wantTerm {
t.Errorf("block char_offset = %v, want %d UTF-16 units", charOff, tc.wantTerm)
}
if bookOff == nil || *bookOff <= 0 {
t.Errorf("book offset = %v, want a positive book-wide rune offset", bookOff)
}
// Round trip: the healed CFI must verify exactly, with the
// same UTF-16 block handle and a stable book offset.
finalCFI2, _, _, charOff2, bookOff2, healedPct2, healed2, err := VerifyProgressAnchor(path, finalCFI, tc.context, 0.3)
if err != nil {
t.Fatalf("re-verify error: %v", err)
}
if healed2 || healedPct2 != nil {
t.Errorf("exact round trip should not heal (healed=%v)", healed2)
}
if finalCFI2 != wantCFI {
t.Errorf("re-verified CFI = %q, want %q", finalCFI2, wantCFI)
}
if charOff2 == nil || *charOff2 != tc.wantTerm {
t.Errorf("re-verified block char_offset = %v, want %d", charOff2, tc.wantTerm)
}
if bookOff2 == nil || bookOff == nil || *bookOff2 != *bookOff {
t.Errorf("book offset unstable: %v vs %v", bookOff2, bookOff)
}
})
}
}
// The book-wide offset must order anchors the way the book orders them:
// a later paragraph in the same document has a strictly larger
// character_offset.
func TestBookOffsetOrdering(t *testing.T) {
path := writeTestEPUB(t)
_, _, _, _, book1, _, _, err1 := VerifyProgressAnchor(path, dashwoodCFI, dashwoodText, 0.3)
_, _, _, _, book2, _, _, err2 := VerifyProgressAnchor(path, estateCFI, estateText, 0.3)
if err1 != nil || err2 != nil {
t.Fatalf("verify errors: %v %v", err1, err2)
}
if book1 == nil || book2 == nil {
t.Fatalf("book offsets missing: %v %v", book1, book2)
}
if *book2 <= *book1 {
t.Errorf("estate offset %d should exceed dashwood offset %d", *book2, *book1)
}
}
+47
View File
@@ -274,8 +274,18 @@ func EstimatedPages(totalCharacters int64) int {
type ProgressService struct {
db *database.Queries
connManager *ConnectionManager
mediaPaths MediaPathResolver
}
// MediaPathResolver resolves a media item's library-relative file path to
// an absolute path, so the progress service can verify submitted anchors
// against the book itself.
type MediaPathResolver interface {
ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error)
}
func (s *ProgressService) SetMediaPathResolver(r MediaPathResolver) { s.mediaPaths = r }
func NewProgressService(db *database.Queries, connManager *ConnectionManager) *ProgressService {
return &ProgressService{db: db, connManager: connManager}
}
@@ -435,6 +445,43 @@ func (s *ProgressService) SaveProgress(ctx context.Context, req SaveProgressRequ
}
}
// Canonical position authority: resolve the submitted CFI against the
// book itself, cross-check the submitted context text, and heal the
// anchor by text search on any mismatch. Clients submit what their
// renderer can reliably observe (percentage + visible text); the
// server owns the structural math and keeps buggy clients from
// poisoning stored positions.
if !isFixed && s.mediaPaths != nil && params.ContextText.Valid &&
isConvertible(string(formatGroup)) && formatGroup != "" {
if path, perr := s.mediaPaths.ResolveMediaPath(ctx, mediaItem.LibraryID, mediaItem.FilePath); perr == nil && path != "" {
submittedCFI := ""
if params.Epubcfi.Valid {
submittedCFI = params.Epubcfi.String
}
submittedPct := 0.0
if params.Percentage.Valid {
submittedPct = params.Percentage.Float64
}
if finalCFI, _, _, _, bookOff, healedPct, healed, verr := VerifyProgressAnchor(path, submittedCFI, params.ContextText.String, submittedPct); verr != nil {
log.Printf("Bookhoard: progress anchor verify failed for %s: %v", req.MediaItemID.String(), verr)
} else {
if healed || (!params.Epubcfi.Valid && finalCFI != "") {
params.Epubcfi = pgtype.Text{String: finalCFI, Valid: true}
if healedPct != nil {
params.Percentage = pgtype.Float8{Float64: *healedPct, Valid: true}
}
}
// The verified book-wide offset refreshes the column on
// EVERY verified save — not only heals — so it never goes
// stale behind the anchor (and a block-relative offset is
// never written into the book-level column).
if bookOff != nil {
params.CharacterOffset = pgtype.Int8{Int64: int64(*bookOff), Valid: true}
}
}
}
}
conflictDetected := false
if hasExisting && existing.LastSyncSource.Valid && existing.LastSyncSource.String != req.Source {
if existing.LastSyncTimestamp.Valid {
+10
View File
@@ -0,0 +1,10 @@
package version
// Version is the app version. It defaults to "dev" for local builds and is
// overridden at build time in release images via:
//
// go build -ldflags "-X bookhoard/internal/version.Version=v1.5"
//
// The release workflow passes the git tag (e.g. v1.5) as the APP_VERSION
// Docker build arg, which the Dockerfile forwards to this flag.
var Version = "dev"
+1 -1
View File
@@ -12,7 +12,7 @@
"dev": "npm run build:ts:dev && npm run build:css"
},
"dependencies": {
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#e448d36",
"@bookhoard/foliate-js": "git+https://git.linuxhg.com/Bookhoard/foliate-js.git#e16530a",
"alpinejs": "^3.15.8",
"chart.js": "^4.5.1",
"highlight.js": "^11.11.1",
+12
View File
@@ -71,6 +71,18 @@ templ AdminArchived(user User, items []ArchivedItem, retentionDays int) {
<td class="px-4 py-3" style="color: var(--text-primary)">{ item.LibraryName }</td>
<td class="px-4 py-3" style="color: var(--text-secondary)">{ item.Status }</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
if item.MatchID != "" {
<button
type="button"
data-merge-source={ item.ID }
data-merge-target={ item.MatchID }
class="btn btn-ghost text-xs"
title={ "Merge this archived copy's reading data into the active copy \"" + item.MatchTitle + "\" (" + item.MatchLibraryName + ") and remove the archived entry" }
>
@Icon("copy", "h-3.5 w-3.5")
Merge
</button>
}
<button
type="button"
data-unarchive={ item.ID }
+79 -22
View File
@@ -57,7 +57,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(retentionDays)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 30, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 30, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -113,7 +113,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 65, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 65, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -131,7 +131,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 67, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 67, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -149,7 +149,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 69, Col: 103}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 69, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -162,7 +162,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.LibraryName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 71, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 71, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -175,26 +175,83 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 72, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 72, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"px-4 py-3 text-right whitespace-nowrap\"><button type=\"button\" data-unarchive=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"px-4 py-3 text-right whitespace-nowrap\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 76, Col: 37}
if item.MatchID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button type=\"button\" data-merge-source=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 77, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-merge-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.MatchID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 78, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-ghost text-xs\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("Merge this archived copy's reading data into the active copy \"" + item.MatchTitle + "\" (" + item.MatchLibraryName + ") and remove the archived entry")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 80, Col: 174}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("copy", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Merge</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<button type=\"button\" data-unarchive=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"btn btn-ghost text-xs\" title=\"Restore to libraries now; if the file is still gone the next scan hides it again\">")
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 88, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"btn btn-ghost text-xs\" title=\"Restore to libraries now; if the file is still gone the next scan hides it again\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -202,20 +259,20 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Restore</button> <button type=\"button\" data-delete-archived=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "Restore</button> <button type=\"button\" data-delete-archived=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 85, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 97, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-ghost text-xs\" title=\"Delete this item and its reading history permanently\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"btn btn-ghost text-xs\" title=\"Delete this item and its reading history permanently\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -223,17 +280,17 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Delete</button></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete</button></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</tbody></table></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</tbody></table></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></main></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div></main></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+16 -16
View File
@@ -59,7 +59,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("conflict-" + conflict.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 27, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 27, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -80,7 +80,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d copies", len(conflict.Items)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 33, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 33, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -93,7 +93,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.LibraryName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 36, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 36, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -106,7 +106,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.SHAShort)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 38, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 38, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -119,7 +119,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/admin/hash-conflicts/" + conflict.ID + "/resolve")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 46, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 46, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -132,7 +132,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("#conflict-" + conflict.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 48, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 48, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -194,7 +194,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 71, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 71, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -212,7 +212,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 73, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 73, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -230,7 +230,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 75, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 75, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -243,7 +243,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f MB", float64(item.FileSize)/(1024*1024)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 77, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 77, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -261,7 +261,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.UsageSummary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 79, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 79, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -279,7 +279,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.UsageSummary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 81, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 81, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -297,7 +297,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/admin/hash-conflicts/" + conflictID + "/resolve")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 87, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 87, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -310,7 +310,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf(`{"action": "keep", "keep_uuid": "%s"}`, item.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 88, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 88, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -323,7 +323,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#conflict-" + conflictID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 89, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 89, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -383,7 +383,7 @@ func AdminHashConflicts(user User, conflicts []HashConflictData) templ.Component
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
+19 -2
View File
@@ -151,10 +151,18 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
@Icon("close", "h-5 w-5")
</button>
</div>
<p class="mb-6" style="color: var(--text-primary)">
<p class="mb-3" style="color: var(--text-primary)">
Are you sure you want to delete <strong id="delete-library-name"></strong>?
This will remove the library and all its folder mappings. Media files will not be deleted.
</p>
<ul class="mb-6 space-y-2 text-sm list-disc list-inside" style="color: var(--text-primary)">
<li>
<strong>Permanently removed, no archive or undo:</strong>
the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.
</li>
<li>
<strong>Not touched:</strong> media files on disk.
</li>
</ul>
<div class="flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
<button type="button" id="delete-library-confirm" class="btn btn-danger">
@@ -201,6 +209,15 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
htmx.process(content);
document.getElementById('folder-browser-modal').classList.remove('hidden');
}
// Close the delete-library modal after a successful delete: the
// htmx swap already removes the deleted row from the libraries
// list, but the modal lives outside the swapped container and
// would otherwise stay open.
document.getElementById('delete-library-confirm').addEventListener('htmx:afterRequest', function(evt) {
if (evt.detail.successful) {
document.getElementById('delete-library-modal').classList.add('hidden');
}
});
</script>
</body>
</html>
+37 -37
View File
@@ -65,7 +65,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 42, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 42, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -140,7 +140,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button></div><p class=\"mb-6\" style=\"color: var(--text-primary)\">Are you sure you want to delete <strong id=\"delete-library-name\"></strong>? This will remove the library and all its folder mappings. Media files will not be deleted.</p><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('delete-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" id=\"delete-library-confirm\" class=\"btn btn-danger\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button></div><p class=\"mb-3\" style=\"color: var(--text-primary)\">Are you sure you want to delete <strong id=\"delete-library-name\"></strong>?</p><ul class=\"mb-6 space-y-2 text-sm list-disc list-inside\" style=\"color: var(--text-primary)\"><li><strong>Permanently removed, no archive or undo:</strong> the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.</li><li><strong>Not touched:</strong> media files on disk.</li></ul><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('delete-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" id=\"delete-library-confirm\" class=\"btn btn-danger\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -148,7 +148,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Delete</button></div></div></div><script>\n\t\t\t\tfunction toggleLibraryPanel(btn) {\n\t\t\t\t\tvar libId = btn.dataset.libId;\n\t\t\t\t\tvar panel = document.getElementById('library-panel-' + libId);\n\t\t\t\t\tif (panel.innerHTML.trim() !== '') {\n\t\t\t\t\t\tpanel.innerHTML = '';\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtmx.ajax('GET', '/admin/library/' + libId + '/panel', {\n\t\t\t\t\t\t\ttarget: '#library-panel-' + libId,\n\t\t\t\t\t\t\tswap: 'innerHTML'\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfunction openEditModal(id, name, description) {\n\t\t\t\t\tdocument.getElementById('edit-library-form').setAttribute('hx-put', '/admin/library/' + id);\n\t\t\t\t\tdocument.getElementById('edit-library-name').value = name;\n\t\t\t\t\tdocument.getElementById('edit-library-desc').value = description;\n\t\t\t\t\thtmx.process(document.getElementById('edit-library-form'));\n\t\t\t\t\tdocument.getElementById('edit-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openDeleteModal(id, name) {\n\t\t\t\t\tconst btn = document.getElementById('delete-library-confirm');\n\t\t\t\t\tbtn.setAttribute('hx-delete', '/admin/library/' + id);\n\t\t\t\t\tbtn.setAttribute('hx-target', '#libraries-container');\n\t\t\t\t\tbtn.setAttribute('hx-swap', 'innerHTML');\n\t\t\t\t\tdocument.getElementById('delete-library-name').textContent = name;\n\t\t\t\t\thtmx.process(btn);\n\t\t\t\t\tdocument.getElementById('delete-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openFolderBrowser(targetInputId, libraryId) {\n\t\t\t\t\tconst content = document.getElementById('folder-browser-content');\n\t\t\t\t\tcontent.setAttribute('hx-get', '/admin/library/browse');\n\t\t\t\t\tcontent.setAttribute('hx-vals', '{\"target_input\": \"' + targetInputId + '\", \"library_id\": \"' + libraryId + '\"}');\n\t\t\t\t\tcontent.setAttribute('hx-trigger', 'load');\n\t\t\t\t\thtmx.process(content);\n\t\t\t\t\tdocument.getElementById('folder-browser-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t</script></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Delete</button></div></div></div><script>\n\t\t\t\tfunction toggleLibraryPanel(btn) {\n\t\t\t\t\tvar libId = btn.dataset.libId;\n\t\t\t\t\tvar panel = document.getElementById('library-panel-' + libId);\n\t\t\t\t\tif (panel.innerHTML.trim() !== '') {\n\t\t\t\t\t\tpanel.innerHTML = '';\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtmx.ajax('GET', '/admin/library/' + libId + '/panel', {\n\t\t\t\t\t\t\ttarget: '#library-panel-' + libId,\n\t\t\t\t\t\t\tswap: 'innerHTML'\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfunction openEditModal(id, name, description) {\n\t\t\t\t\tdocument.getElementById('edit-library-form').setAttribute('hx-put', '/admin/library/' + id);\n\t\t\t\t\tdocument.getElementById('edit-library-name').value = name;\n\t\t\t\t\tdocument.getElementById('edit-library-desc').value = description;\n\t\t\t\t\thtmx.process(document.getElementById('edit-library-form'));\n\t\t\t\t\tdocument.getElementById('edit-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openDeleteModal(id, name) {\n\t\t\t\t\tconst btn = document.getElementById('delete-library-confirm');\n\t\t\t\t\tbtn.setAttribute('hx-delete', '/admin/library/' + id);\n\t\t\t\t\tbtn.setAttribute('hx-target', '#libraries-container');\n\t\t\t\t\tbtn.setAttribute('hx-swap', 'innerHTML');\n\t\t\t\t\tdocument.getElementById('delete-library-name').textContent = name;\n\t\t\t\t\thtmx.process(btn);\n\t\t\t\t\tdocument.getElementById('delete-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openFolderBrowser(targetInputId, libraryId) {\n\t\t\t\t\tconst content = document.getElementById('folder-browser-content');\n\t\t\t\t\tcontent.setAttribute('hx-get', '/admin/library/browse');\n\t\t\t\t\tcontent.setAttribute('hx-vals', '{\"target_input\": \"' + targetInputId + '\", \"library_id\": \"' + libraryId + '\"}');\n\t\t\t\t\tcontent.setAttribute('hx-trigger', 'load');\n\t\t\t\t\thtmx.process(content);\n\t\t\t\t\tdocument.getElementById('folder-browser-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\t// Close the delete-library modal after a successful delete: the\n\t\t\t\t// htmx swap already removes the deleted row from the libraries\n\t\t\t\t// list, but the modal lives outside the swapped container and\n\t\t\t\t// would otherwise stay open.\n\t\t\t\tdocument.getElementById('delete-library-confirm').addEventListener('htmx:afterRequest', function(evt) {\n\t\t\t\t\tif (evt.detail.successful) {\n\t\t\t\t\t\tdocument.getElementById('delete-library-modal').classList.add('hidden');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -211,7 +211,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 229, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 246, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -229,7 +229,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 231, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 248, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -251,7 +251,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 236, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 253, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -273,7 +273,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(library.FolderCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 241, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 258, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -291,7 +291,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + library.ID + "/scan")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 247, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 264, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -312,7 +312,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 257, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 274, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -333,7 +333,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-panel-" + library.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 266, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 283, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -408,7 +408,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(folder.FolderPath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 288, Col: 99}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 305, Col: 99}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -421,7 +421,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 291, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 308, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil {
@@ -434,7 +434,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"folder_path": "` + folder.FolderPath + `"}`)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 292, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 309, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -447,7 +447,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 293, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 310, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -478,7 +478,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 303, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 320, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -491,7 +491,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 303, Col: 124}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 320, Col: 124}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -504,7 +504,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 307, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 324, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
@@ -517,7 +517,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 314, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 331, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -530,7 +530,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 315, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 332, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
@@ -587,7 +587,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/visibility")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 344, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 361, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
@@ -600,7 +600,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"user_id": "` + u.ID + `"}`)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 345, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 362, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
@@ -613,7 +613,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 347, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 364, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
@@ -626,7 +626,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 350, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 367, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@@ -639,7 +639,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 351, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 368, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -667,7 +667,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var26 templ.SafeURL
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs("/admin/libraries/" + libraryID + "/issues")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 360, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 377, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@@ -694,7 +694,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(issueCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 365, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 382, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@@ -721,7 +721,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 374, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 391, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
@@ -734,7 +734,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 375, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 392, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil {
@@ -747,7 +747,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 376, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 393, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil {
@@ -768,7 +768,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 384, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 401, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
@@ -781,7 +781,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 385, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 402, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
@@ -839,7 +839,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 400, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 417, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
@@ -857,7 +857,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 406, Col: 117}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 423, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil {
@@ -888,7 +888,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 419, Col: 118}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 436, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
if templ_7745c5c3_Err != nil {
@@ -909,7 +909,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 424, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 441, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -927,7 +927,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(targetInput)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 431, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 448, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
if templ_7745c5c3_Err != nil {
@@ -940,7 +940,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 432, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 449, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
if templ_7745c5c3_Err != nil {
+15 -15
View File
@@ -70,7 +70,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ErrorCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 36, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 36, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -97,7 +97,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.WarningCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 45, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 45, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -124,7 +124,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.InfoCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 54, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 54, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -166,7 +166,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("issue-" + issue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 70, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 70, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -179,7 +179,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 73, Col: 98}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 73, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -192,7 +192,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 74, Col: 96}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 74, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -205,7 +205,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 76, Col: 140}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 76, Col: 140}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -218,7 +218,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 77, Col: 144}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 77, Col: 144}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -231,7 +231,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 78, Col: 139}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 78, Col: 139}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -244,7 +244,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 79, Col: 149}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 79, Col: 149}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -262,7 +262,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 84, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 84, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -280,7 +280,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 86, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 86, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -298,7 +298,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 88, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 88, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -321,7 +321,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + libraryID + "/issues/" + issue.ID + "/" + issue.MediaItemID + "/resolve")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 95, Col: 113}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 95, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -334,7 +334,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("#issue-" + issue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 96, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 96, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
+20 -1
View File
@@ -1,6 +1,9 @@
package templates
import "fmt"
import (
"bookhoard/internal/version"
"fmt"
)
templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) {
<!DOCTYPE html>
@@ -111,6 +114,7 @@ templ AdminSettings(user User, systemConfig map[string]string, scanSettings Scan
@ScanSettingsSection(scanSettings)
@TunableSettingsSection(liveGroups, false)
@TunableSettingsSection(restartGroups, true)
@AboutSection()
</div>
</main>
</body>
@@ -255,3 +259,18 @@ templ TunableSettingRow(e SettingEntry) {
<span id={ "status-" + e.Key } class="text-xs w-24 text-right" style="color: var(--text-secondary)"></span>
</div>
}
// AboutSection shows build metadata, rendered at the bottom of the System
// Settings page. Add future rows (disk space, uptime, Go version, …) to the
// list below.
templ AboutSection() {
<div class="mt-6 card p-6">
<div class="flex items-center gap-2 mb-4">
@Icon("info", "h-5 w-5 shrink-0")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">About</h3>
</div>
<div class="space-y-2 text-sm" style="color: var(--text-secondary);">
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Version:</span> { version.Version }</p>
</div>
</div>
}
+78 -18
View File
@@ -8,7 +8,10 @@ package templates
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
import (
"bookhoard/internal/version"
"fmt"
)
func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@@ -67,7 +70,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 31, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 34, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -93,7 +96,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 45, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 48, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -370,7 +373,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 106, Col: 145}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 109, Col: 145}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -383,7 +386,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 107, Col: 144}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 110, Col: 144}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -396,7 +399,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 108, Col: 152}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 111, Col: 152}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -418,6 +421,10 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AboutSection().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</div></main></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -472,7 +479,7 @@ func ScanSettingsSection(scanSettings ScanSettingsData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", scanSettings.ScanPollIntervalSeconds))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 154, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 158, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -564,7 +571,7 @@ func TunableSettingsSection(groups []SettingGroup, restartRequired bool) templ.C
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(g.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 194, Col: 112}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 198, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -622,7 +629,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 209, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 213, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -640,7 +647,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.Key)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 211, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 215, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -658,7 +665,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.Key)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 213, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 217, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -676,7 +683,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#status-" + e.Key)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 219, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 223, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -689,7 +696,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Key)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 223, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 227, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -732,7 +739,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 233, Col: 20}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 237, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -750,7 +757,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Min)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 235, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 239, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
@@ -769,7 +776,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Max)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 238, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 242, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -792,7 +799,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 246, Col: 20}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 250, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
@@ -818,7 +825,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("status-" + e.Key)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 255, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 259, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
@@ -832,4 +839,57 @@ func TunableSettingRow(e SettingEntry) templ.Component {
})
}
// AboutSection shows build metadata, rendered at the bottom of the System
// Settings page. Add future rows (disk space, uptime, Go version, …) to the
// list below.
func AboutSection() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("info", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">About</h3></div><div class=\"space-y-2 text-sm\" style=\"color: var(--text-secondary);\"><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Version:</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(version.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 273, Col: 134}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</p></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
+4 -4
View File
@@ -60,7 +60,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.LibraryCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 32, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 32, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -81,7 +81,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 39, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 39, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -102,7 +102,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.UserCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 46, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 46, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -123,7 +123,7 @@ func Admin(user User, stats AdminStats) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.DeviceCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 53, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 53, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
+10 -10
View File
@@ -57,7 +57,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("user-" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 42, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 42, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -78,7 +78,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 50, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 50, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -101,7 +101,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 59, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 59, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -124,7 +124,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 77, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 77, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -137,7 +137,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("#role-result-" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 78, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 78, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -170,7 +170,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("role-result-" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 87, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 87, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -188,7 +188,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(user.CreatedAt, currentUser.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 92, Col: 126}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 92, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -201,7 +201,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/users/" + user.ID + "/profile-modal")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 98, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 98, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -240,7 +240,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 117, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 117, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -253,7 +253,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue("#user-" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 118, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 118, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
+13 -13
View File
@@ -42,7 +42,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 8, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 8, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -60,7 +60,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 10, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 10, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -78,7 +78,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 12, Col: 120}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 12, Col: 120}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 14, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 14, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -114,7 +114,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 16, Col: 88}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 16, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -127,7 +127,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 24, Col: 131}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 24, Col: 131}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -140,7 +140,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 28, Col: 128}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 28, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -179,7 +179,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 45, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 45, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -197,7 +197,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 47, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 47, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -215,7 +215,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 49, Col: 120}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 49, Col: 120}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -233,7 +233,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 51, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 51, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -251,7 +251,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 53, Col: 88}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 53, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -304,7 +304,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 70, Col: 136}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 70, Col: 136}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
+70 -70
View File
@@ -49,7 +49,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 25, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 25, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -88,7 +88,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Source)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 57, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 57, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -101,7 +101,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(data.Timestamp, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 60, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 60, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -119,7 +119,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", data.Data["percentage"].(float64)*100))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 66, Col: 73}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 66, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -147,7 +147,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["page"].(float64)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 74, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 74, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -160,7 +160,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["total_pages"].(float64)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 74, Col: 128}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 74, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -174,7 +174,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["page"].(float64)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 76, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 76, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -194,7 +194,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.Data["epubcfi"].(string))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 82, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 82, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -212,7 +212,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 87, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 87, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -235,7 +235,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 98, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 98, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -248,7 +248,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 102, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 102, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -261,7 +261,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 105, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 105, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -279,7 +279,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 109, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 109, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -292,7 +292,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 109, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 109, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -311,7 +311,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 114, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 114, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -371,7 +371,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 148, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 148, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -392,7 +392,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.HighlightsCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 163, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 163, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -405,7 +405,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.NotesCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 167, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 167, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -418,7 +418,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(len(book.DeletedAnnotations))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 171, Col: 106}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 171, Col: 106}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@@ -441,7 +441,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(len(book.DeletedAnnotations))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 183, Col: 125}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 183, Col: 125}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -459,7 +459,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(ann.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 190, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 190, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
@@ -472,7 +472,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(ann.AnnotationType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 193, Col: 119}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 193, Col: 119}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@@ -485,7 +485,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(ann.DeletedAt, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 195, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 195, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -498,7 +498,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("restoreDeletedAnnotation('" + ann.AnnotationType + "', '" + ann.ID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 200, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 200, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
@@ -519,7 +519,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue("purgeDeletedAnnotation('" + ann.AnnotationType + "', '" + ann.ID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 207, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 207, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
@@ -545,7 +545,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(ann.DisplayText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 217, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 217, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@@ -569,7 +569,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(ann.SecondaryText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 222, Col: 103}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 222, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@@ -647,7 +647,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 275, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 275, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
@@ -660,7 +660,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 276, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 276, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
@@ -678,7 +678,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 282, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 282, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil {
@@ -712,7 +712,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 317, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 317, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
if templ_7745c5c3_Err != nil {
@@ -725,7 +725,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 321, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 321, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil {
@@ -738,7 +738,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.Description))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 326, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 326, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
@@ -751,7 +751,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.Summary))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 331, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 331, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -777,7 +777,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(tag)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 344, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 344, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
if templ_7745c5c3_Err != nil {
@@ -795,7 +795,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f", book.CommunityRating.Float64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 378, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 378, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
if templ_7745c5c3_Err != nil {
@@ -824,7 +824,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Publisher))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 396, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 396, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
if templ_7745c5c3_Err != nil {
@@ -837,7 +837,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(formatDateForInput(book.DatePublished))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 401, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 401, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41)
if templ_7745c5c3_Err != nil {
@@ -850,7 +850,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Edition))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 406, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 406, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42)
if templ_7745c5c3_Err != nil {
@@ -863,7 +863,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Language))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 410, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 410, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43)
if templ_7745c5c3_Err != nil {
@@ -876,7 +876,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Genre))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 414, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 414, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44)
if templ_7745c5c3_Err != nil {
@@ -889,7 +889,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var45 string
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.CopyrightYear.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 419, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 419, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45)
if templ_7745c5c3_Err != nil {
@@ -918,7 +918,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Series))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 437, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 437, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46)
if templ_7745c5c3_Err != nil {
@@ -931,7 +931,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesNumber.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 443, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 443, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47)
if templ_7745c5c3_Err != nil {
@@ -944,7 +944,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesCount.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 449, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 449, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48)
if templ_7745c5c3_Err != nil {
@@ -957,7 +957,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var49 string
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.Volume.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 455, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 455, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
if templ_7745c5c3_Err != nil {
@@ -986,7 +986,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var50 string
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Isbn))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 474, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 474, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
if templ_7745c5c3_Err != nil {
@@ -999,7 +999,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var51 string
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Asin))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 478, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 478, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
if templ_7745c5c3_Err != nil {
@@ -1012,7 +1012,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var52 string
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.GoodreadsID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 482, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 482, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
if templ_7745c5c3_Err != nil {
@@ -1025,7 +1025,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var53 string
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.OpenlibraryID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 486, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 486, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
if templ_7745c5c3_Err != nil {
@@ -1038,7 +1038,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var54 string
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.GoogleBooksID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 490, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 490, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54)
if templ_7745c5c3_Err != nil {
@@ -1051,7 +1051,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var55 string
templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.WebUrl))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 494, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 494, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var55)
if templ_7745c5c3_Err != nil {
@@ -1080,7 +1080,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var56 string
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "unknown")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 512, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 512, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56)
if templ_7745c5c3_Err != nil {
@@ -1093,7 +1093,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var57 string
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "no")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 513, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 513, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57)
if templ_7745c5c3_Err != nil {
@@ -1106,7 +1106,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var58 string
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 514, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 514, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58)
if templ_7745c5c3_Err != nil {
@@ -1119,7 +1119,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var59 string
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes_and_right_to_left")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 515, Col: 113}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 515, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
if templ_7745c5c3_Err != nil {
@@ -1132,7 +1132,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var60 string
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "auto")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 521, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 521, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
if templ_7745c5c3_Err != nil {
@@ -1145,7 +1145,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var61 string
templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "ltr")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 522, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 522, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var61)
if templ_7745c5c3_Err != nil {
@@ -1158,7 +1158,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var62 string
templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "rtl")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 523, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 523, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
if templ_7745c5c3_Err != nil {
@@ -1171,7 +1171,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var63 string
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "vertical")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 524, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 524, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
if templ_7745c5c3_Err != nil {
@@ -1184,7 +1184,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var64 string
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.AgeRating))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 529, Col: 81}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 529, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
if templ_7745c5c3_Err != nil {
@@ -1197,7 +1197,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var65 string
templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.StoryArc))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 533, Col: 79}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 533, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65)
if templ_7745c5c3_Err != nil {
@@ -1210,7 +1210,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var66 string
templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Imprint))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 537, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 537, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
if templ_7745c5c3_Err != nil {
@@ -1223,7 +1223,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var67 string
templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ScanInformation))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 541, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 541, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
if templ_7745c5c3_Err != nil {
@@ -1236,7 +1236,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var68 string
templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.MetadataNotes))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 546, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 546, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
if templ_7745c5c3_Err != nil {
@@ -1275,7 +1275,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var69 string
templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.PageCount.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 570, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 570, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
if templ_7745c5c3_Err != nil {
@@ -1288,7 +1288,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var70 string
templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(stringSliceToString(book.Contributors))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 575, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 575, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70)
if templ_7745c5c3_Err != nil {
@@ -1301,7 +1301,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var71 string
templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MimeType))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 580, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 580, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
if templ_7745c5c3_Err != nil {
@@ -1314,7 +1314,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var72 string
templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 585, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 585, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
if templ_7745c5c3_Err != nil {
@@ -1327,7 +1327,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var73 string
templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.FileLocation)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 591, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 591, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var73)
if templ_7745c5c3_Err != nil {
+63 -63
View File
@@ -45,7 +45,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 18, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 18, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -58,7 +58,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.FormatGroup)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 26, Col: 39}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 26, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -71,7 +71,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(uuidToString(book.LibraryID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 27, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 27, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -84,7 +84,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", getBookRating(book.Rating)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 28, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 28, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -97,7 +97,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflictID(book.ActiveConflict))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 29, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 29, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -110,7 +110,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflictWinnerSource(book.ActiveConflict))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 30, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 30, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -136,7 +136,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: url('" + book.CoverImagePath.String + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 36, Col: 101}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 36, Col: 101}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -167,7 +167,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 48, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 48, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -180,7 +180,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 49, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 49, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -198,7 +198,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 57, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 57, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -216,7 +216,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 65, Col: 117}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 65, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -234,7 +234,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 68, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 68, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -252,7 +252,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("window.location.href = '/readers/" + uuidToString(book.ID) + "'")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 74, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 74, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -331,7 +331,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.NotesCount + book.HighlightsCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 107, Col: 144}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 107, Col: 144}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -380,7 +380,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 162, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 162, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -403,7 +403,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(book.Series.String) + "&library_id=" + uuidToString(book.LibraryID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 169, Col: 125}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 169, Col: 125}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -416,7 +416,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Series.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 173, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 173, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -434,7 +434,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.SeriesNumber.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 175, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 175, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -454,7 +454,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ToUpper(book.ReadingDirection.String))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 180, Col: 142}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 180, Col: 142}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -473,7 +473,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(book.AgeRating.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 183, Col: 123}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 183, Col: 123}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@@ -498,7 +498,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(book.StoryArc.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 189, Col: 122}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 189, Col: 122}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -526,7 +526,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var23 templ.SafeURL
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs("/tags/detail?name=" + url.QueryEscape(tag) + "&library_id=" + uuidToString(book.LibraryID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 196, Col: 109}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 196, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@@ -539,7 +539,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tag)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 200, Col: 16}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 200, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@@ -595,7 +595,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(book.MetadataNotes.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 230, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 230, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -636,7 +636,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%; background-color: var(--accent);", book.ReadingProgress.Percentage.Float64*100))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 247, Col: 124}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 247, Col: 124}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@@ -649,7 +649,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 254, Col: 134}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 254, Col: 134}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@@ -667,7 +667,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 259, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 259, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@@ -680,7 +680,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 259, Col: 131}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 259, Col: 131}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@@ -699,7 +699,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 265, Col: 140}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 265, Col: 140}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
@@ -718,7 +718,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 271, Col: 110}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 271, Col: 110}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
@@ -746,7 +746,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Publisher.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 284, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 284, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
@@ -765,7 +765,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(book.DatePublished.Time.Format("01-02-2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 290, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 290, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
@@ -784,7 +784,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(book.Isbn.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 296, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 296, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
@@ -803,7 +803,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(book.Language.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 302, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 302, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
@@ -822,7 +822,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(book.Edition.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 308, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 308, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
@@ -841,7 +841,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(book.PageCount.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 314, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 314, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -860,7 +860,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(book.Genre.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 320, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 320, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
@@ -879,7 +879,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(book.SeriesCount.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 326, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 326, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil {
@@ -898,7 +898,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(book.Volume.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 332, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 332, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil {
@@ -917,7 +917,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(book.Imprint.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 338, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 338, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
@@ -936,7 +936,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(book.CopyrightYear.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 344, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 344, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil {
@@ -955,7 +955,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ReplaceAll(book.MangaType.String, "_", " "))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 350, Col: 118}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 350, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil {
@@ -974,7 +974,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(book.ScanInformation.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 356, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 356, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil {
@@ -993,7 +993,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var45 string
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(altSeries)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 362, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 362, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
if templ_7745c5c3_Err != nil {
@@ -1012,7 +1012,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(book.Contributors, ", "))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 368, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 368, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
if templ_7745c5c3_Err != nil {
@@ -1030,7 +1030,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(book.MimeType.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 373, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 373, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
if templ_7745c5c3_Err != nil {
@@ -1048,7 +1048,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(formatFileSize(book.FileSize.Int64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 378, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 378, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil {
@@ -1067,7 +1067,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var49 string
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(book.FileLocation)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 384, Col: 113}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 384, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
if templ_7745c5c3_Err != nil {
@@ -1095,7 +1095,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var50 templ.SafeURL
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("goodreads", book.GoodreadsID.String, book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 391, Col: 106}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 391, Col: 106}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
if templ_7745c5c3_Err != nil {
@@ -1113,7 +1113,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var51 templ.SafeURL
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("goodreads", "", book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 393, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 393, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
if templ_7745c5c3_Err != nil {
@@ -1132,7 +1132,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var52 templ.SafeURL
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("openlibrary", book.OpenlibraryID.String, book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 396, Col: 110}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 396, Col: 110}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
if templ_7745c5c3_Err != nil {
@@ -1150,7 +1150,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var53 templ.SafeURL
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("openlibrary", "", book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 398, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 398, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
if templ_7745c5c3_Err != nil {
@@ -1169,7 +1169,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var54 templ.SafeURL
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("googlebooks", book.GoogleBooksID.String, book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 401, Col: 110}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 401, Col: 110}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
if templ_7745c5c3_Err != nil {
@@ -1187,7 +1187,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var55 templ.SafeURL
templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("googlebooks", "", book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 403, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 403, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
if templ_7745c5c3_Err != nil {
@@ -1206,7 +1206,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var56 templ.SafeURL
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("amazon", book.Asin.String, book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 406, Col: 96}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 406, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
if templ_7745c5c3_Err != nil {
@@ -1224,7 +1224,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var57 templ.SafeURL
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("amazon", "", book.Isbn, book.Title, book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 408, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 408, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
if templ_7745c5c3_Err != nil {
@@ -1243,7 +1243,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var58 templ.SafeURL
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinURLErrs(book.WebUrl.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 411, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 411, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
if templ_7745c5c3_Err != nil {
@@ -1256,7 +1256,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var59 string
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(getDomainName(book.WebUrl.String))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 411, Col: 238}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 411, Col: 238}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
if templ_7745c5c3_Err != nil {
@@ -1289,7 +1289,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var60 templ.SafeURL
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinURLErrs("/collections/" + uuidToString(col.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 423, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 423, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))
if templ_7745c5c3_Err != nil {
@@ -1302,7 +1302,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var61 string
templ_7745c5c3_Var61, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("border-color: " + col.Color.String + "; background-color: var(--bg-primary); text-decoration: none;")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 425, Col: 118}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 425, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
if templ_7745c5c3_Err != nil {
@@ -1315,7 +1315,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var62 string
templ_7745c5c3_Var62, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("color: " + col.Color.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 427, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 427, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
if templ_7745c5c3_Err != nil {
@@ -1328,7 +1328,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var63 string
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 427, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 427, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
if templ_7745c5c3_Err != nil {
@@ -1341,7 +1341,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
var templ_7745c5c3_Var64 string
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 428, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 428, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
if templ_7745c5c3_Err != nil {
+20 -20
View File
@@ -97,7 +97,7 @@ func BookShelf(
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(uuidToString(filter.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 93, Col: 135}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 93, Col: 135}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -110,7 +110,7 @@ func BookShelf(
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(filter.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 95, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 95, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -173,7 +173,7 @@ func BookShelf(
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libraries))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 147, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 147, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -191,7 +191,7 @@ func BookShelf(
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libraries))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 149, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 149, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -212,7 +212,7 @@ func BookShelf(
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 154, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -225,7 +225,7 @@ func BookShelf(
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 154, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -238,7 +238,7 @@ func BookShelf(
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 154, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -256,7 +256,7 @@ func BookShelf(
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 156, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -269,7 +269,7 @@ func BookShelf(
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 156, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -282,7 +282,7 @@ func BookShelf(
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 156, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -301,7 +301,7 @@ func BookShelf(
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 34}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 159, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
@@ -314,7 +314,7 @@ func BookShelf(
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 159, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -327,7 +327,7 @@ func BookShelf(
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 159, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -345,7 +345,7 @@ func BookShelf(
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 34}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 161, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -358,7 +358,7 @@ func BookShelf(
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 161, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -371,7 +371,7 @@ func BookShelf(
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 161, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -400,7 +400,7 @@ func BookShelf(
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 275, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 275, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -423,7 +423,7 @@ func BookShelf(
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset-limit))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 284, Col: 126}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 284, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -454,7 +454,7 @@ func BookShelf(
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 293, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 293, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -467,7 +467,7 @@ func BookShelf(
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset+limit))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 298, Col: 126}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 298, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
+9 -9
View File
@@ -41,7 +41,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(pageTitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 14, Col: 21}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 14, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -62,7 +62,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(backUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 22, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 22, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -83,7 +83,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(backLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 24, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 24, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(badgeIcon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 31, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 31, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -109,7 +109,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(badgeLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 31, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 31, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -122,7 +122,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 33, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 33, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -135,7 +135,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(books)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 34, Col: 125}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 34, Col: 125}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -168,7 +168,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emptyIcon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 44, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 44, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -181,7 +181,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(emptyMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 46, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 46, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
+7 -7
View File
@@ -72,7 +72,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + collection.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 23, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 23, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -85,7 +85,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 26, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 26, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -98,7 +98,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 32, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 32, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -111,7 +111,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 45, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 45, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -124,7 +124,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 58, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 58, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -137,7 +137,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Color)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 74, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 74, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -155,7 +155,7 @@ func CollectionModal(collection CollectionData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 116, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 116, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
+4 -4
View File
@@ -36,7 +36,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 9, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 9, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -65,7 +65,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 23, Col: 176}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 23, Col: 176}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -78,7 +78,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 25, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 25, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -99,7 +99,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 45, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 45, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
+29 -29
View File
@@ -97,7 +97,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 71, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 71, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -110,7 +110,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(col.Color)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 74, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 74, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -123,7 +123,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 77, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 77, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -136,7 +136,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID + "/edit-modal")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 80, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 80, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -157,7 +157,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + col.ID + "")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 89, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 89, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -178,7 +178,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 99, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 99, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -191,7 +191,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 100, Col: 81}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 100, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -246,7 +246,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 117, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 117, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -279,7 +279,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 132, Col: 166}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 132, Col: 166}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -292,7 +292,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 134, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 134, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -305,7 +305,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 135, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 135, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -355,7 +355,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 179, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 179, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -368,7 +368,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 179, Col: 112}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 179, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -381,7 +381,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 179, Col: 147}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 179, Col: 147}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -404,7 +404,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 194, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 194, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -417,7 +417,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 196, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 196, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -435,7 +435,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 201, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 201, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -453,7 +453,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var20 templ.SafeURL
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 206, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 206, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -471,7 +471,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 209, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 209, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
@@ -551,7 +551,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 385, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 385, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
@@ -564,7 +564,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 386, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 386, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
@@ -577,7 +577,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.IsSystem)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 387, Col: 39}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 387, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
@@ -620,7 +620,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.toggleBook('" + book.MediaItemID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 397, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 397, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
@@ -638,7 +638,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 401, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 401, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
@@ -651,7 +651,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 401, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 401, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
@@ -669,7 +669,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 403, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 403, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil {
@@ -687,7 +687,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.isSelected('" + book.MediaItemID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 406, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 406, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil {
@@ -700,7 +700,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.isSelected('" + book.MediaItemID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 411, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 411, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
@@ -713,7 +713,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 418, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 418, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
+9 -9
View File
@@ -54,7 +54,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(total)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 45, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 45, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -67,7 +67,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(unresolved)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 49, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 49, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -80,7 +80,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(total - unresolved)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 53, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 53, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -128,7 +128,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflict.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 111, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 111, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -141,7 +141,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflict.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 117, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 117, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -162,7 +162,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.MediaItemTitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 125, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 125, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -175,7 +175,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.ConflictType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 126, Col: 96}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 126, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -188,7 +188,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(conflict.CreatedAt, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 133, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 133, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -206,7 +206,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.ResolvedBy)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 135, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 135, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -65,7 +65,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 74, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `custom_section.templ`, Line: 74, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -78,7 +78,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 74, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `custom_section.templ`, Line: 74, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
+29 -29
View File
@@ -104,7 +104,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 40, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 40, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -117,7 +117,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.IsSystem)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 41, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 41, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -130,7 +130,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 45, Col: 130}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 45, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -143,7 +143,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 47, Col: 100}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 47, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -161,7 +161,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 49, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 49, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -184,7 +184,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(section.ViewAllURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 55, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 55, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -210,7 +210,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 68, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 68, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -231,7 +231,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("carousel-track-" + section.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 76, Col: 39}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 76, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -249,7 +249,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 81, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 81, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -289,7 +289,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 95, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 95, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
@@ -339,7 +339,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var14 templ.SafeURL
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 108, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -357,7 +357,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 115, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 115, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -370,7 +370,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 116, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 116, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
@@ -388,7 +388,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 124, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 124, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
@@ -406,7 +406,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 130, Col: 117}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 130, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
@@ -419,7 +419,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 131, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -437,7 +437,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 134, Col: 100}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 134, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
@@ -450,7 +450,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 135, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 135, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@@ -473,7 +473,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var22 templ.SafeURL
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 143, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 143, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -486,7 +486,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("Resolve progress conflict for " + item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 145, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 145, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
@@ -512,7 +512,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var24 templ.SafeURL
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs("/readers/" + item.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 152, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 152, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@@ -525,7 +525,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("Read " + item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 154, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 154, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil {
@@ -593,7 +593,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 184, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
@@ -606,7 +606,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 185, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
@@ -627,7 +627,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 190, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@@ -640,7 +640,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 192, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 192, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
@@ -668,7 +668,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 202, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 202, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
@@ -701,7 +701,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 128}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 228, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
@@ -714,7 +714,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 235, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 235, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil {
+16 -16
View File
@@ -111,7 +111,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("showShelfMappings('" + device.ID.String() + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 64, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 64, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -132,7 +132,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue("showDeviceSettings('" + device.ID.String() + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 67, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 67, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -153,7 +153,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 72, Col: 102}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 72, Col: 102}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -166,7 +166,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 73, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 73, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -199,7 +199,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(*device.LastSync, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 86, Col: 104}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 86, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -227,7 +227,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(*device.LastSeen, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 94, Col: 104}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 94, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -255,7 +255,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(baseURL + "/api/sync/kobo/" + device.AuthToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 110, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 110, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -282,7 +282,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(device.AuthToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 132, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 132, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -308,7 +308,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/devices/" + device.ID.String() + "/regenerate-token")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 147, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 147, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -349,7 +349,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 168, Col: 87}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 168, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -362,7 +362,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 170, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 170, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -375,7 +375,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 170, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 170, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -388,7 +388,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("approveDevice('" + reg.RegistrationID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 174, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 174, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -401,7 +401,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("rejectDevice('" + reg.RegistrationID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 177, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 177, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -440,7 +440,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(baseURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 242, Col: 148}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 242, Col: 148}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -453,7 +453,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("copyToClipboard('" + baseURL + "', 'Server URL')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 244, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 244, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
+15 -15
View File
@@ -38,7 +38,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 11, Col: 21}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 11, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -60,7 +60,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -102,7 +102,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 50, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 50, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -133,7 +133,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var6 templ.SafeURL
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 56, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -146,7 +146,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 57, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 57, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -159,7 +159,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 58, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 58, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -187,7 +187,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var9 templ.SafeURL
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 65, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 65, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -200,7 +200,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 66, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 66, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -213,7 +213,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 67, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 67, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -257,7 +257,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 82, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 82, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -270,7 +270,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 82, Col: 129}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 82, Col: 129}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -293,7 +293,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 86, Col: 104}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 86, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -324,7 +324,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var15 templ.SafeURL
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 96, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 96, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -337,7 +337,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level) + "; color: var(--text-secondary);")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 98, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 98, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -350,7 +350,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 100, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 100, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -36,7 +36,7 @@ func ErrorPage(message string, errorType string) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(message)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/error.templ`, Line: 84, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `error.templ`, Line: 84, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -49,7 +49,7 @@ func ErrorPage(message string, errorType string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/error.templ`, Line: 90, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `error.templ`, Line: 90, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -36,7 +36,7 @@ func FilterItem(id string, name string) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 6, Col: 21}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `filter_item.templ`, Line: 6, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -49,7 +49,7 @@ func FilterItem(id string, name string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 14, Col: 9}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `filter_item.templ`, Line: 14, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
+22 -22
View File
@@ -53,7 +53,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -83,7 +83,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -113,7 +113,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -143,7 +143,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -173,7 +173,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -203,7 +203,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil {
@@ -260,7 +260,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeTheme('" + opt.Name + "'); openPanel = null")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 103, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 103, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -273,7 +273,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 105, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 105, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
@@ -286,7 +286,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + opt.Color)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 108, Col: 130}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 108, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -299,7 +299,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 109, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 109, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -321,7 +321,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -352,7 +352,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeWoodPaneling('" + w.Name + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 120, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 120, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
@@ -365,7 +365,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(w.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 122, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 122, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
@@ -388,7 +388,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: url(/static/textures/thumb-" + w.Name + ".webp); background-size: cover;")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 128, Col: 191}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 128, Col: 191}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -406,7 +406,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(w.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 130, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 130, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@@ -446,7 +446,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var24).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil {
@@ -476,7 +476,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var26).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
@@ -506,7 +506,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var28).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil {
@@ -536,7 +536,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var30).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
@@ -566,7 +566,7 @@ func Header(user User, currentPath string) templ.Component {
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var32).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil {
@@ -646,7 +646,7 @@ func SidebarUserMenu(user User) templ.Component {
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 220, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 220, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
@@ -730,7 +730,7 @@ func SidebarSignIn(currentPath string) templ.Component {
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 268, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 268, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
if templ_7745c5c3_Err != nil {
+1 -1
View File
@@ -45,7 +45,7 @@ func Icon(name string, extra string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/icons.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `icons.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -41,7 +41,7 @@ func Index(loggedIn bool) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 24, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 24, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -54,7 +54,7 @@ func Index(loggedIn bool) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 24, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 24, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
+13 -13
View File
@@ -36,7 +36,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 16, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 16, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -56,7 +56,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 20, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -69,7 +69,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 20, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -82,7 +82,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 20, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -100,7 +100,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 22, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -113,7 +113,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 22, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -126,7 +126,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 22, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -145,7 +145,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 25, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -158,7 +158,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 25, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -171,7 +171,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 25, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -189,7 +189,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 27, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
@@ -202,7 +202,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 27, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -215,7 +215,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 27, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -41,7 +41,7 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 24, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `login.templ`, Line: 24, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -54,7 +54,7 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 24, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `login.templ`, Line: 24, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
+6 -6
View File
@@ -36,7 +36,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(actionURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 5, Col: 20}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 5, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -57,7 +57,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 22, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 22, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -70,7 +70,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 31, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 31, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -83,7 +83,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.FirstName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 40, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 40, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.LastName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 50, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 50, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -475,7 +475,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/password/" + user.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 189, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 189, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -44,7 +44,7 @@ func ProfileModal(user User) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_modal.templ`, Line: 14, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -57,7 +57,7 @@ func ProfileModal(user User) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_modal.templ`, Line: 14, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
+15 -15
View File
@@ -74,7 +74,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 44, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 44, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -87,7 +87,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID.String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 54, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 54, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -100,7 +100,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 58, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 58, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -118,7 +118,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 61, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 61, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -136,7 +136,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", item.ProgressPercentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 64, Col: 127}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 64, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -149,7 +149,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 68, Col: 162}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 68, Col: 162}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -164,7 +164,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 77, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 77, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -174,7 +174,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", item.ProgressPercentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 79, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 79, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -185,7 +185,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 82, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 82, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -199,7 +199,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 88, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 88, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -212,7 +212,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 93, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 93, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -225,7 +225,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 94, Col: 98}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 94, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -251,7 +251,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 101, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 101, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -264,7 +264,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 102, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 102, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -291,7 +291,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 108, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 108, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
+13 -13
View File
@@ -62,7 +62,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.PendingCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 41, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 41, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -75,7 +75,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ProcessingCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 45, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 45, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -88,7 +88,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.CompletedCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 49, Col: 99}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 49, Col: 99}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -101,7 +101,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.FailedCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 53, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 53, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -149,7 +149,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 130, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 130, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -162,7 +162,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.SyncType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 131, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 131, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -175,7 +175,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Priority)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 133, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 133, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -188,7 +188,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 136, Col: 34}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 136, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -206,7 +206,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(*item.MediaTitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 138, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 138, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -220,7 +220,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Attempts)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 39}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 142, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -233,7 +233,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.MaxAttempts)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 142, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -246,7 +246,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.CreatedAt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 143, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 143, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -272,7 +272,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(*item.ErrorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 148, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 148, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
+95 -98
View File
@@ -16,6 +16,7 @@ func readerInitExpr(metadata ReaderMetadata) string {
"formatGroup": metadata.FormatGroup,
"readingDirection": metadata.ReadingDirection,
"mangaType": metadata.MangaType,
"treatAsComic": metadata.TreatAsComic,
}
jsonBytes, _ := json.Marshal(config)
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
@@ -23,7 +24,7 @@ func readerInitExpr(metadata ReaderMetadata) string {
templ Reader(user User, metadata ReaderMetadata) {
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="overscroll-none">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
@@ -38,7 +39,7 @@ templ Reader(user User, metadata ReaderMetadata) {
<body
x-data="readerShell"
x-init={ readerInitExpr(metadata) }
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
class={ "theme-" + user.Theme + " h-screen overflow-hidden overscroll-none" }
>
<!-- Reading surface: edge-to-edge. Chrome overlays translucently;
drawers and scrim float above everything. -->
@@ -68,9 +69,7 @@ templ Reader(user User, metadata ReaderMetadata) {
</svg>
</div>
</div>
@ReaderChrome(metadata)
<!-- Drawer scrim -->
<div
x-show="tocOpen || settingsOpen || bookmarksOpen || searchOpen"
@@ -79,7 +78,6 @@ templ Reader(user User, metadata ReaderMetadata) {
class="drawer-scrim"
aria-hidden="true"
></div>
<!-- TOC drawer (left) -->
<div
id="toc-drawer"
@@ -96,7 +94,6 @@ templ Reader(user User, metadata ReaderMetadata) {
>
@ReaderTOCDrawer()
</div>
<!-- Settings drawer (right) -->
<div
id="settings-drawer"
@@ -113,7 +110,6 @@ templ Reader(user User, metadata ReaderMetadata) {
>
@ReaderSettingsDrawer()
</div>
<!-- Selection popover (highlights) -->
<div
id="selection-popover"
@@ -139,7 +135,13 @@ templ Reader(user User, metadata ReaderMetadata) {
:style="'background-color:' + c"
:title="selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'"
:aria-label="'Highlight color ' + c"
@click="selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())"
@click="selectionPopover.noteOpen
? (selectionPopover.mode === 'create'
? (selectionPopover.color = c)
: (selectionPopover.color = c, saveHighlightChanges(true)))
: (selectionPopover.mode === 'create'
? createHighlight(c)
: (selectionPopover.color = c, saveHighlightChanges()))"
></button>
</template>
<div class="w-px h-5 reader-sep"></div>
@@ -188,12 +190,11 @@ templ Reader(user User, metadata ReaderMetadata) {
type="button"
class="flex-1 py-1 text-xs border rounded hover:opacity-80"
style="border-color: var(--border);"
@click="selectionPopover.noteOpen = false"
@click="hideSelectionPopover()"
>Cancel</button>
</div>
</div>
</div>
<!-- Annotations drawer (right): highlights / notes / bookmarks -->
<div
id="annotations-drawer"
@@ -210,7 +211,6 @@ templ Reader(user User, metadata ReaderMetadata) {
>
@ReaderAnnotationsDrawer()
</div>
<!-- Search drawer (right, reflowable only) -->
<div
id="search-drawer"
@@ -227,7 +227,6 @@ templ Reader(user User, metadata ReaderMetadata) {
>
@ReaderSearchDrawer()
</div>
<!-- Shortcuts & gestures help modal -->
<div
x-show="helpOpen"
@@ -263,17 +262,14 @@ templ Reader(user User, metadata ReaderMetadata) {
<div class="help-row"><span>Settings</span><span class="help-keys"><kbd class="kbd">s</kbd></span></div>
<div class="help-row"><span>Add bookmark</span><span class="help-keys"><kbd class="kbd">b</kbd></span></div>
<div class="help-row"><span>Close menus / toggle chrome</span><span class="help-keys"><kbd class="kbd">Esc</kbd></span></div>
<h4 class="reader-help-section" x-show="isFixedLayout">Zoom &amp; pan <span class="help-note">(comics, PDFs)</span></h4>
<div class="help-row" x-show="isFixedLayout"><span>Zoom in / out / reset</span><span class="help-keys"><kbd class="kbd">+</kbd><kbd class="kbd">−</kbd><kbd class="kbd">0</kbd> or wheel</span></div>
<div class="help-row" x-show="isFixedLayout"><span>Move the page</span><span class="help-keys">drag with the mouse</span></div>
<div class="help-row" x-show="isFixedLayout"><span>Recenter <span class="help-note">(keeps zoom)</span></span><span class="help-keys">⊙ button</span></div>
<div class="help-row" x-show="isPDF"><span>Select text in a PDF</span><span class="help-keys">Smart mode: drag over text · <kbd class="kbd">Shift</kbd>+drag anywhere · Text mode</span></div>
<h4 class="reader-help-section">Highlights</h4>
<div class="help-row"><span>Create</span><span class="help-keys">select text → pick a color</span></div>
<div class="help-row"><span>Edit / delete</span><span class="help-keys">click the highlight</span></div>
<h4 class="reader-help-section">Touch</h4>
<div class="help-row"><span>Page turn</span><span class="help-keys">swipe, or tap outer edges</span></div>
<div class="help-row"><span>Toggle chrome</span><span class="help-keys">tap center</span></div>
@@ -295,7 +291,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
<div class="flex items-center gap-1">
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
<button x-show="!isFixedLayout || isPDF" @click="toggleSearch()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Search (/)">🔍</button>
<button x-show="(!isFixedLayout || isPDF) && !treatAsComic" @click="toggleSearch()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Search (/)">🔍</button>
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Annotations">📝</button>
<button @click="toggleSettings()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Settings (s)">Aa</button>
<button @click="toggleHelp()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 text-sm font-bold" title="Shortcuts & gestures (?)" aria-label="Shortcuts and gestures">?</button>
@@ -338,7 +334,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
<div class="flex items-center gap-1">
<div class="w-px h-6 reader-sep"></div>
<div id="progress-display" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">—</span>
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">—</span>
</div>
<div class="w-px h-6 reader-sep"></div>
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
@@ -348,92 +344,93 @@ templ ReaderChrome(metadata ReaderMetadata) {
<div x-show="isFixedLayout">
<!-- Full toolbar: desktop/tablet (>= 768px, md). Wraps gracefully in between. -->
<div class="hidden md:flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1">
<div class="flex items-center gap-0.5">
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left (←)" aria-label="Go left">
<div class="flex items-center gap-0.5">
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left (←)" aria-label="Go left">
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
<path d="M 15 6 L 9 12 L 15 18"></path>
</svg>
</button>
<button x-show="backStack.length > 0" @click="goBackToLocation()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Return to previous location (Alt+←)" aria-label="Return to previous location">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z"></path>
<circle cx="12" cy="9.5" r="2"></circle>
</svg>
</button>
</div>
<input
id="progress-slider-fx"
type="range"
min="0"
max="1"
step="any"
@input="goToFraction($event.target.value)"
class="grow min-w-[3rem]"
/>
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right (→)" aria-label="Go right">
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
<path d="M 15 6 L 9 12 L 15 18"></path>
<path d="M 9 6 L 15 12 L 9 18"></path>
</svg>
</button>
<button x-show="backStack.length > 0" @click="goBackToLocation()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Return to previous location (Alt+←)" aria-label="Return to previous location">
<div class="w-px h-6 mx-1 reader-sep"></div>
<!-- Fit mode + zoom cluster (not applicable in webtoon flow) -->
<div class="flex items-center gap-1" x-show="comicFlow !== 'webtoon'">
<select
@change="applyFitMode($event.target.value)"
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
title="Fit mode"
aria-label="Fit mode"
>
<option value="fit-page">Fit Page</option>
<option value="fit-width">Fit Width</option>
</select>
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom Out (-)" aria-label="Zoom out">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 5 10 L 15 10"></path>
</svg>
</button>
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom (0)" aria-label="Reset zoom">
<span x-text="zoomPercent + '%'">100%</span>
</button>
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom In (+)" aria-label="Zoom in">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
</svg>
</button>
<!-- Recenter: reset pan, keep zoom -->
<button @click="recenterView()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Recenter page (reset pan, keep zoom)" aria-label="Recenter page">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="12" cy="12" r="4"></circle>
<path d="M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22"></path>
</svg>
</button>
<!-- Magnifier -->
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="9" cy="9" r="5"></circle>
<path d="M 13 13 L 18 18"></path>
</svg>
</button>
</div>
<!-- Pointer mode (PDF only): Smart / Pan / Text — hidden for
comic-treated PDFs: no text selection, pan only -->
<div class="reader-seg shrink-0" x-show="isPDF && !treatAsComic" role="group" aria-label="Pointer mode" title="Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.">
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')" title="Smart: drag text to select, drag blank to pan">Smart</button>
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')" title="Pan: drag anywhere to move">Pan</button>
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
</div>
<!-- Double page spread (not applicable in webtoon flow) -->
<button x-show="comicFlow !== 'webtoon'" @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z"></path>
<circle cx="12" cy="9.5" r="2"></circle>
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
</svg>
</button>
</div>
<input
id="progress-slider-fx"
type="range"
min="0"
max="1"
step="any"
@input="goToFraction($event.target.value)"
class="grow min-w-[3rem]"
/>
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right (→)" aria-label="Go right">
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
<path d="M 9 6 L 15 12 L 9 18"></path>
</svg>
</button>
<div class="w-px h-6 mx-1 reader-sep"></div>
<!-- Fit mode + zoom cluster (not applicable in webtoon flow) -->
<div class="flex items-center gap-1" x-show="comicFlow !== 'webtoon'">
<select
@change="applyFitMode($event.target.value)"
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
title="Fit mode"
aria-label="Fit mode"
>
<option value="fit-page">Fit Page</option>
<option value="fit-width">Fit Width</option>
</select>
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom Out (-)" aria-label="Zoom out">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 5 10 L 15 10"></path>
</svg>
</button>
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom (0)" aria-label="Reset zoom">
<span x-text="zoomPercent + '%'">100%</span>
</button>
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom In (+)" aria-label="Zoom in">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
</svg>
</button>
<!-- Recenter: reset pan, keep zoom -->
<button @click="recenterView()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Recenter page (reset pan, keep zoom)" aria-label="Recenter page">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="12" cy="12" r="4"></circle>
<path d="M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22"></path>
</svg>
</button>
<!-- Magnifier -->
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="9" cy="9" r="5"></circle>
<path d="M 13 13 L 18 18"></path>
</svg>
</button>
</div>
<!-- Pointer mode (PDF only): Smart / Pan / Text -->
<div class="reader-seg shrink-0" x-show="isPDF" role="group" aria-label="Pointer mode" title="Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.">
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')" title="Smart: drag text to select, drag blank to pan">Smart</button>
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')" title="Pan: drag anywhere to move">Pan</button>
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
</div>
<!-- Double page spread (not applicable in webtoon flow) -->
<button x-show="comicFlow !== 'webtoon'" @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
</svg>
</button>
<!-- Progress + TOC -->
<div class="flex items-center gap-1">
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
<span x-text="progressMain">—</span>
<!-- Progress + TOC -->
<div class="flex items-center gap-1">
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
<span x-text="progressMain">—</span>
</div>
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
</div>
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
</div>
</div>
<!-- Compact row (phone-sized windows): single line + ⋯ overflow.
Standard mobile-reader pattern: paging, slider, progress stay;
@@ -528,7 +525,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
<span>Night mode</span>
<button @click="fxInvert = !fxInvert; applyFxFilter(true)" class="reader-popover-btn text-xs px-2" :class="fxInvert ? 'text-blue-400' : ''" x-text="fxInvert ? 'On' : 'Off'">Off</button>
</div>
<div class="reader-tools-row" x-show="isPDF">
<div class="reader-tools-row" x-show="isPDF && !treatAsComic">
<span>Pointer</span>
<div class="reader-seg" role="group" aria-label="Pointer mode">
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')">Smart</button>
@@ -700,7 +697,7 @@ templ ReaderAnnotationsDrawer() {
@keydown.enter="renameBookmark(bookmark)"
@keydown.escape="bookmark.renameOpen = false"
placeholder="Bookmark name…"
></input>
/>
<div class="flex gap-2 mt-1">
<button @click="renameBookmark(bookmark)" class="flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
<button @click="bookmark.renameOpen = false" class="flex-1 py-1 text-xs border rounded hover:opacity-80" style="border-color: var(--border);">Cancel</button>
File diff suppressed because one or more lines are too long
+12 -12
View File
@@ -99,7 +99,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 50, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 50, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -125,7 +125,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 58, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 58, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -138,7 +138,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 58, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 58, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -156,7 +156,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 62, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 62, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -224,7 +224,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var7 templ.SafeURL
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(series.Name))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 79, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 79, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -246,7 +246,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -269,7 +269,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(cover)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 84, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 84, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -282,7 +282,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
@@ -295,7 +295,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(series.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 86, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 86, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil {
@@ -327,7 +327,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(series.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 98, Col: 99}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 98, Col: 99}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -340,7 +340,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.BookCount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 100, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 100, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -353,7 +353,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.TotalInSeries))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 100, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 100, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
+1 -1
View File
@@ -37,7 +37,7 @@ func ErrorToast(message string) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("showErrorToast('" + message + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/toast.templ`, Line: 5, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `toast.templ`, Line: 5, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
+19 -10
View File
@@ -52,17 +52,23 @@ type FolderData struct {
}
// ArchivedItem feeds the admin archived-items page: media hidden from
// libraries because their files vanished, with their pending fate.
// libraries because their files vanished, with their pending fate. When an
// active item with identical content (same SHA-256) exists, MatchID and
// friends are set so the UI can offer merging the archived row's reading
// data into its active twin.
type ArchivedItem struct {
ID string
Title string
Author string
LibraryName string
FilePath string
MissingScans int32
ArchivedAt *time.Time
PurgeAt *time.Time
Status string
ID string
Title string
Author string
LibraryName string
FilePath string
MissingScans int32
ArchivedAt *time.Time
PurgeAt *time.Time
Status string
MatchID string
MatchTitle string
MatchLibraryName string
}
type DirEntry struct {
@@ -243,6 +249,9 @@ type ReaderMetadata struct {
LibraryID string `json:"library_id"`
TotalCharacters int64 `json:"total_characters"`
EstimatedPages int `json:"estimated_pages"`
// TreatAsComic: fixed-layout item in a comics/manga library gets
// comic reader treatment (bookmarks only — no highlights, no search).
TreatAsComic bool `json:"treat_as_comic"`
}
type ReadingProgress struct {
+10 -10
View File
@@ -105,7 +105,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.ProgressID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 63, Col: 100}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 63, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -118,7 +118,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.TitleFromDevice)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 63, Col: 136}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 63, Col: 136}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
@@ -151,7 +151,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 77, Col: 111}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 77, Col: 111}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -164,7 +164,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 79, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 79, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -177,7 +177,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 79, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 79, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -194,7 +194,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 88, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 88, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -212,7 +212,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.SHA256)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 94, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 94, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -225,7 +225,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 94, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 94, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -243,7 +243,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 99, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 99, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -261,7 +261,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 105, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 105, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
+38
View File
@@ -339,6 +339,39 @@ async function deleteArchivedItem(id: string): Promise<void> {
}
}
// Merge an archived item's reading data into its active duplicate with the
// same content hash (POST /api/media-items/:id/merge), then remove the
// archived entry. Covers copy-then-delete-later moves between libraries.
async function mergeArchivedItem(sourceId: string, targetId: string): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
if (
!window.confirm(
"Merge this archived copy's reading progress, notes, and highlights into the active copy? The archived entry is removed.",
)
) {
return;
}
try {
const resp = await fetch(`/api/media-items/${sourceId}/merge`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ target_id: targetId }),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || "Failed to merge item");
}
showToast("Reading data merged into active copy", "success");
setTimeout(() => window.location.reload(), 500);
} catch (e) {
showToast(e instanceof Error ? e.message : "Failed to merge item", "error");
}
}
// The archived-items page renders per-row buttons with data attributes
// (escaping-safe); bind them here. Module scripts run after DOM parse.
document.querySelectorAll<HTMLElement>("[data-unarchive]").forEach((el) => {
@@ -349,6 +382,11 @@ document.querySelectorAll<HTMLElement>("[data-delete-archived]").forEach((el) =>
deleteArchivedItem(el.dataset.deleteArchived || ""),
);
});
document.querySelectorAll<HTMLElement>("[data-merge-source]").forEach((el) => {
el.addEventListener("click", () =>
mergeArchivedItem(el.dataset.mergeSource || "", el.dataset.mergeTarget || ""),
);
});
export {
hideScanProgress,
+601 -130
View File
@@ -358,6 +358,15 @@ const getCSS = ({
text-align: ${justify ? "justify" : "start"} !important;
hyphens: ${hyphenate ? "auto" : "none"};
}
/* Chrome's touch word-selection walks hyphen fragments: dragging from a
line-start word re-anchors at the line-end fragment (silently dropping
the selected word) and fast drags overshoot into the next column.
Coarse pointers get whole words only. */
@media (pointer: coarse) {
p, li, blockquote, dd {
hyphens: ${hyphenate ? "manual" : "none"};
}
}
[align="left"] { text-align: left; }
[align="right"] { text-align: right; }
[align="center"] { text-align: center; }
@@ -389,6 +398,10 @@ document.addEventListener("alpine:init", () => {
isFixedLayout: false,
isPDF: false,
isComic: false,
// Fixed-layout item in a comics/manga library (e.g. a manga PDF):
// comic reader treatment — bookmarks only, no text-selection
// highlights, no in-book search.
treatAsComic: false as boolean,
comicFlow: "paged" as string,
fxBrightness: 1 as number,
fxContrast: 1 as number,
@@ -404,12 +417,20 @@ document.addEventListener("alpine:init", () => {
pointerFine: true as boolean,
fxZoomed: false as boolean,
tapZonesEnabled: true as boolean,
tapZoneSize: 30 as number,
tapZoneSize: 12 as number,
tapZoneTimer: null as ReturnType<typeof setTimeout> | null,
// Any live text selection, host document or content iframe. Fed by
// selectionchange listeners (touch devices); tap zones stand down
// while one exists.
anySelection: false as boolean,
// The section document most recently loaded (for clearing its
// selection after actions that consume it).
currentDoc: null as any,
// Timestamp of the last touch press (pointer:coarse surfaces); a
// selection appearing during a short press is Chrome's tap-to-select,
// not a selection gesture.
lastTouchDownT: 0 as number,
selPopTimer: null as ReturnType<typeof setTimeout> | null,
highlightItems: [] as {
id: string;
text: string;
@@ -571,8 +592,10 @@ document.addEventListener("alpine:init", () => {
formatGroup: string;
readingDirection: string;
mangaType: string;
treatAsComic: boolean;
}) {
this.mediaItemId = config.mediaItemId;
this.treatAsComic = config.treatAsComic;
// Reading state (position, bookmarks, annotations) is never baked
// into the rendered page: the web reader is intrinsically tied to
// the server, so it reads all of it from the APIs at open time —
@@ -598,7 +621,7 @@ document.addEventListener("alpine:init", () => {
this.chromeBehavior =
behavior === "always-visible" ? "always-visible" : "auto-hide";
this.tapZonesEnabled = this.settings.tap_zones_enabled ?? true;
this.tapZoneSize = this.settings.tap_zone_size || 30;
this.tapZoneSize = this.settings.tap_zone_size || 12;
const mode = this.settings.pdf_interaction_mode;
if (mode === "pan" || mode === "text" || mode === "select") {
this.interactionMode = mode;
@@ -679,6 +702,7 @@ document.addEventListener("alpine:init", () => {
}
this.view.addEventListener("load", (e: any) => {
const { doc, index } = e.detail;
this.currentDoc = doc;
const link = doc.createElement("link");
link.rel = "stylesheet";
link.href = "/static/reader-fonts.css";
@@ -702,64 +726,61 @@ document.addEventListener("alpine:init", () => {
this.noteSelectionActivity(
!!sel && !sel.isCollapsed && !!sel.toString(),
);
// The gesture takeover swallows pointerup for touch selection
// drags, so the popover's pointerup trigger never fires for
// them; a selection that has been stable for a moment is the
// settled gesture — open the popover for it.
if (
this.isFixedLayout &&
!this.treatAsComic &&
doc.querySelector(".textLayer")
)
this.schedulePDFSelectionPopover(doc, index);
else if (!this.isFixedLayout)
this.scheduleSelectionPopover(doc, index);
});
}
// Clicks in the book dismiss the popover in ANY mode and for ANY
// content doc (EPUB sections and fixed-layout pages alike): iframe
// events never bubble to the host document (so the host
// outside-click dismiss never sees them). Clicking a painted
// highlight still works: this hides, then foliate's
// show-annotation re-opens it in edit mode.
doc.addEventListener(
"pointerdown",
() => {
if (this.selectionPopover.open)
this.hideSelectionPopover();
},
{ passive: true },
);
// Text selection → highlight popover (reflowable EPUB only;
// fixed-layout highlight overlays are a later milestone).
if (!this.isFixedLayout) {
const checkSelection = () => {
const checkSelection = (e?: PointerEvent) => {
const sel = doc.getSelection();
// Chrome on touch selects the word under a quick tap — that is
// a page-turn tap, not a selection gesture; clear it and show
// nothing. Real hold selections never reach this pointerup
// (the gesture takeover swallows it) — they open the popover
// through the selectionchange debounce below.
if (
e && e.pointerType === "touch" && this.lastTouchDownT &&
Date.now() - this.lastTouchDownT < 300
) {
sel?.removeAllRanges();
return;
}
if (!sel || sel.isCollapsed || !sel.rangeCount) {
if (this.selectionPopover.mode === "create")
this.hideSelectionPopover();
return;
}
const range = sel.getRangeAt(0);
const text = sel.toString().replace(/\s+/g, " ").trim();
if (!text) return;
let cfi: string;
let cfiEnd: string;
try {
cfi = this.view.getCFI(index, range);
// Collapse to the end point for a distinct end anchor —
// KOReader sync renders the highlight box from pos0/pos1, and
// pos1 == pos0 would be a degenerate (zero-length) range.
const endRange = range.cloneRange();
endRange.collapse(false);
cfiEnd = this.view.getCFI(index, endRange);
} catch {
return;
}
const frame = doc.defaultView?.frameElement as HTMLElement | null;
const iframeRect = frame?.getBoundingClientRect();
const rect = range.getBoundingClientRect();
this.openSelectionPopover({
mode: "create",
x: (iframeRect?.left ?? 0) + rect.left + rect.width / 2,
y: (iframeRect?.top ?? 0) + rect.top,
text,
cfi,
cfiEnd,
});
this.openPopoverForSelection(sel, doc, index);
};
doc.addEventListener(
"pointerup",
() => setTimeout(checkSelection, 0),
{ passive: true },
);
// Clicks in the book dismiss the popover in ANY mode: iframe
// events never bubble to the host document (so the host
// outside-click dismiss never sees them), and the collapsed
// check above only covers create mode — edit mode had no
// outside-click path at all, leaving Esc as the only way out.
// Clicking a painted highlight still works: this hides, then
// foliate's show-annotation re-opens it in edit mode.
doc.addEventListener(
"pointerdown",
() => {
if (this.selectionPopover.open)
this.hideSelectionPopover();
},
(e: PointerEvent) => setTimeout(() => checkSelection(e), 0),
{ passive: true },
);
doc.addEventListener(
@@ -774,66 +795,29 @@ document.addEventListener("alpine:init", () => {
// Detected structurally: renderer.isPDF isn't set until the first
// spread renders (during view.init), which is after this listener
// attaches — the stale copy here would always be falsy.
if (this.isFixedLayout && doc.querySelector(".textLayer")) {
const checkPDFSelection = () => {
const sel = doc.getSelection();
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
const range = sel.getRangeAt(0);
const text = sel.toString().replace(/\s+/g, " ").trim();
if (!text) return;
// Denominator: the element whose post-transform screen rect IS
// the visible page. For PDFs that's the rendered canvas — pdf.js
// scales <html> by 1/devicePixelRatio, so documentElement's rect
// is dpr× too small and would inflate every fraction (highlight
// shifted right/oversized on any dpr != 1 display). The canvas's
// rect is in the same transform-inclusive space as the textLayer
// span rects, so the dpr scaling cancels exactly.
const denom =
(doc.querySelector("#canvas canvas") as HTMLElement) ||
(doc.querySelector("img") as HTMLElement);
let dr = denom?.getBoundingClientRect();
if (!dr || !dr.width || !dr.height) {
const vw = doc.defaultView;
dr = {
left: 0,
top: 0,
width: vw?.innerWidth || 1,
height: vw?.innerHeight || 1,
} as DOMRect;
}
const rects: number[][] = [];
for (const r of range.getClientRects()) {
const x = (r.left - dr.left) / dr.width;
const y = (r.top - dr.top) / dr.height;
const w = r.width / dr.width;
const h = r.height / dr.height;
if (w > 0 && h > 0) rects.push([x, y, w, h]);
}
if (!rects.length) return;
// Map the first rect to host-space for popover placement.
// The canvas's screen rect maps 1:1 onto the host iframe box
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
// kept general for the comic img fallback.
const frame = doc.defaultView?.frameElement as HTMLElement | null;
if (!frame) return;
const fr = frame.getBoundingClientRect();
const first = range.getBoundingClientRect();
const sx = fr.width / dr.width;
const sy = fr.height / dr.height;
this.pdfSelDoc = doc;
this.openSelectionPopover({
mode: "create",
x: fr.left + first.left * sx + (first.width * sx) / 2,
y: fr.top + first.top * sy,
text,
cfi: "",
pdfPage: index,
pdfRects: rects,
});
};
// Comic-treated PDFs never attach it: no highlights, like comics.
if (
this.isFixedLayout &&
!this.treatAsComic &&
doc.querySelector(".textLayer")
) {
doc.addEventListener(
"pointerup",
() => setTimeout(checkPDFSelection, 0),
(e: PointerEvent) =>
setTimeout(() => {
// Chrome on touch selects the word under a quick tap —
// that is a page-turn tap, not a selection gesture; clear
// it and show nothing (same guard as the reflowable path).
if (
e.pointerType === "touch" &&
this.lastTouchDownT &&
Date.now() - this.lastTouchDownT < 300
) {
doc.getSelection()?.removeAllRanges();
return;
}
this.checkPDFSelectionFor(doc, index);
}, 0),
{ passive: true },
);
}
@@ -885,7 +869,18 @@ document.addEventListener("alpine:init", () => {
this.view.addEventListener("relocate", (e: any) => {
const { fraction, location, pageItem, cfi, tocItem, section } =
e.detail;
this.hideSelectionPopover();
// Only dismiss the popover on an actual position change — the
// paginator also emits no-op relocates (e.g. a tap's page-aligned
// anchor), which would otherwise close a just-opened highlight
// edit popover. NOTE: detail.section is an object (recreated on
// every relocate), so it can't be compared by reference; the
// fraction threshold alone covers section changes (any section
// boundary moves the whole-book fraction far more than 0.1%).
const prev = this.lastRelocateDetail;
const moved =
!prev ||
Math.abs((prev.fraction ?? 0) - (fraction ?? 0)) > 0.001;
if (moved) this.hideSelectionPopover();
this.lastCfi = cfi || "";
this.lastRelocateDetail = {
fraction,
@@ -959,6 +954,9 @@ document.addEventListener("alpine:init", () => {
// Smart|Pan|Text control appear for PDFs.
this.isPDF = !!this.renderer?.isPDF;
if (this.isPDF) {
// Comic-treated PDFs are paged through like comics: no text
// selection, so pointer interactions stay in pan mode.
if (this.treatAsComic) this.interactionMode = "pan";
this.renderer.setAttribute("interaction-mode", this.interactionMode);
}
this.fxZoomed = this.isFixedLayout && this.renderer?.zoom != null;
@@ -1006,6 +1004,10 @@ document.addEventListener("alpine:init", () => {
this.pokeChrome();
},
pokeChrome() {
// Never raise the chrome while a touch selection is live — the
// selection popover owns the screen until the selection is
// consumed or dismissed.
if (this.anySelection) return;
this.chromeVisible = true;
if (this.chromeBehavior !== "auto-hide") return;
if (this.hideTimer) clearTimeout(this.hideTimer);
@@ -1057,6 +1059,15 @@ document.addEventListener("alpine:init", () => {
clearTimeout(this.tapZoneTimer);
this.tapZoneTimer = null;
}
// Hide the chrome the moment a selection appears — it competes
// with the selection popover and handles for screen space.
if (hasSelection && this.chromeVisible) {
this.chromeVisible = false;
if (this.hideTimer) {
clearTimeout(this.hideTimer);
this.hideTimer = null;
}
}
},
attachTapZoneListeners(surface: HTMLElement, isDoc: boolean) {
let downX = 0;
@@ -1080,6 +1091,7 @@ document.addEventListener("alpine:init", () => {
downId = e.pointerId;
moved = false;
longPressed = false;
this.lastTouchDownT = downT;
},
{ passive: true },
);
@@ -1129,16 +1141,26 @@ document.addEventListener("alpine:init", () => {
// No tap actions while a fixed-layout page is zoomed — taps then
// belong to the content (and double-tap zoom).
if (this.isFixedLayout && this.renderer?.zoom != null) return;
const width =
(isDoc
? (surface as any).documentElement.clientWidth
: (surface as HTMLElement).getBoundingClientRect().width) || 1;
const relX =
(e.clientX -
(isDoc
? 0
: (surface as HTMLElement).getBoundingClientRect().left)) /
width;
// Content iframes are laid out at the full section width (the
// paginator shows one column slice of a much wider document), so
// clientX must be mapped into the visible page before it can be
// compared against the tap zones — dividing by the iframe's
// clientWidth puts every tap in the left zone.
let relX: number;
if (isDoc) {
const r: any = this.renderer;
const docWidth = (surface as any).documentElement.clientWidth || 1;
if (r && !r.scrolled && r.size) {
const base = r.start - r.size;
relX = (e.clientX - base) / r.size;
} else {
relX = e.clientX / docWidth;
}
if (this.view?.book?.dir === "rtl") relX = 1 - relX;
} else {
const rect = (surface as HTMLElement).getBoundingClientRect();
relX = (e.clientX - rect.left) / (rect.width || 1);
}
this.routeTapDebounced(relX);
},
{ passive: true },
@@ -1154,6 +1176,13 @@ document.addEventListener("alpine:init", () => {
}
this.tapZoneTimer = setTimeout(() => {
this.tapZoneTimer = null;
// Touch pages by swipe only (the paginator's pan + snap); a tap
// anywhere just toggles the chrome. The edge zones remain for
// mouse-driven taps.
if (window.matchMedia("(pointer: coarse)").matches) {
this.toggleChromeManually();
return;
}
const zone = this.tapZoneSize / 100;
if (relX < zone) this.goLeft();
else if (relX > 1 - zone) this.goRight();
@@ -1199,10 +1228,14 @@ document.addEventListener("alpine:init", () => {
p.noteOpen = !!p.note && opts.mode === "edit";
p.pdfPage = opts.pdfPage ?? -1;
p.pdfRects = opts.pdfRects ?? [];
// Clamp so the popover stays on screen (it anchors bottom-center).
// Clamp so the popover stays on screen (it anchors bottom-center;
// the popover is ~240px wide, so the fixed margins must cover half
// of it). NOTE: do not mutate the position after opening — touching
// the style mid-enter-transition leaves the popover invisible to
// compositor hit-testing (taps fall through to the content iframe).
const w = window.innerWidth;
const h = window.innerHeight;
p.x = Math.min(Math.max(opts.x, 90), w - 90);
p.x = Math.min(Math.max(opts.x, 130), w - 130);
p.y = Math.min(Math.max(opts.y, 60), h - 60);
p.open = true;
},
@@ -1210,6 +1243,266 @@ document.addEventListener("alpine:init", () => {
this.selectionPopover.open = false;
this.selectionPopover.noteOpen = false;
},
scheduleSelectionPopover(doc: any, index: number) {
if (this.selPopTimer) clearTimeout(this.selPopTimer);
this.selPopTimer = setTimeout(() => {
this.selPopTimer = null;
// A tap on a painted highlight re-opens the popover in edit mode
// via its own path — never clobber it.
if (this.selectionPopover.open && this.selectionPopover.mode === "edit")
return;
const sel = doc.getSelection();
if (!sel || sel.isCollapsed || !sel.rangeCount) {
if (
this.selectionPopover.open &&
this.selectionPopover.mode === "create"
)
this.hideSelectionPopover();
this.hideSelectionHandles();
return;
}
// The browser's selection auto-scroll (drags near a viewport
// edge) scrolls the paginated container off the page grid, and
// a selection that crosses the page edge keeps its reveal-scroll
// chasing the off-page text — the view bounces between pages
// forever. Correct the view to the anchor's page and clamp the
// selection to the visible page; scrollBy clamps to the current
// section, so the correction can never cross sections.
const r: any = this.renderer;
if (r && !r.scrolled && r.size && this.view?.book?.dir !== "rtl") {
try {
const anchorProbe = doc.createRange();
anchorProbe.setStart(sel.anchorNode, sel.anchorOffset);
// collapsed ranges report degenerate (0,0) rects — extend by
// one character to get a measurable one
if (
sel.anchorNode?.nodeType === 3 &&
sel.anchorOffset < sel.anchorNode.length
)
anchorProbe.setEnd(sel.anchorNode, sel.anchorOffset + 1);
const rects = anchorProbe.getClientRects();
const ar =
rects[rects.length - 1] || anchorProbe.getBoundingClientRect();
// rect x is already in the iframe's own (content) space,
// i.e. scroll-invariant: page = floor(x / column stride)
const desired = (Math.floor(ar.x / r.size) + 1) * r.size;
const needsScroll = Math.abs(r.start - desired) > r.size * 0.02;
if (needsScroll) r.scrollBy(desired - r.start, 0);
const clamped = this.clampSelectionToPage(sel, doc, r);
if (clamped)
// the clamp's selectionchange re-arms this settle; the
// next pass finds view and selection in place and opens
// the popover
return;
if (needsScroll) {
setTimeout(() => {
const s = doc.getSelection();
if (s && s.rangeCount && !s.isCollapsed && s.type === "Range")
this.openPopoverForSelection(s, doc, index);
}, 350);
return;
}
} catch {
/* anchor geometry unavailable */
}
}
this.openPopoverForSelection(sel, doc, index);
}, 400);
},
// PDF mirror of scheduleSelectionPopover: the gesture takeover
// swallows pointerup for touch selection drags in the fx iframe too,
// so a selection that has been stable for a moment is the settled
// gesture — open the popover for it.
schedulePDFSelectionPopover(doc: any, index: number) {
if (this.selPopTimer) clearTimeout(this.selPopTimer);
this.selPopTimer = setTimeout(() => {
this.selPopTimer = null;
// A tap on a painted highlight re-opens the popover in edit mode
// via its own path — never clobber it.
if (this.selectionPopover.open && this.selectionPopover.mode === "edit")
return;
const sel = doc.getSelection();
if (!sel || sel.isCollapsed || !sel.rangeCount) {
if (
this.selectionPopover.open &&
this.selectionPopover.mode === "create"
)
this.hideSelectionPopover();
return;
}
this.checkPDFSelectionFor(doc, index);
}, 400);
},
checkPDFSelectionFor(doc: any, index: number) {
const sel = doc.getSelection();
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
const range = sel.getRangeAt(0);
const text = sel.toString().replace(/\s+/g, " ").trim();
if (!text) return;
// Denominator: the element whose post-transform screen rect IS
// the visible page. For PDFs that's the rendered canvas — pdf.js
// scales <html> by 1/devicePixelRatio, so documentElement's rect
// is dpr× too small and would inflate every fraction (highlight
// shifted right/oversized on any dpr != 1 display). The canvas's
// rect is in the same transform-inclusive space as the textLayer
// span rects, so the dpr scaling cancels exactly.
const denom =
(doc.querySelector("#canvas canvas") as HTMLElement) ||
(doc.querySelector("img") as HTMLElement);
let dr = denom?.getBoundingClientRect();
if (!dr || !dr.width || !dr.height) {
const vw = doc.defaultView;
dr = {
left: 0,
top: 0,
width: vw?.innerWidth || 1,
height: vw?.innerHeight || 1,
} as DOMRect;
}
const rects: number[][] = [];
for (const r of range.getClientRects()) {
const x = (r.left - dr.left) / dr.width;
const y = (r.top - dr.top) / dr.height;
const w = r.width / dr.width;
const h = r.height / dr.height;
if (w > 0 && h > 0) rects.push([x, y, w, h]);
}
if (!rects.length) return;
// Map the first rect to host-space for popover placement.
// The canvas's screen rect maps 1:1 onto the host iframe box
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
// kept general for the comic img fallback.
const frame = doc.defaultView?.frameElement as HTMLElement | null;
if (!frame) return;
const fr = frame.getBoundingClientRect();
const first = range.getBoundingClientRect();
const sx = fr.width / dr.width;
const sy = fr.height / dr.height;
// On touch the browser's own selection menu attaches ABOVE the
// selection and its drag handles sit just below it — place ours
// below both (same +90 clearance the reflowable path uses). Fine
// pointers (desktop) keep the above-the-selection placement.
const coarse = window.matchMedia("(pointer: coarse)").matches;
const y = coarse
? fr.top + first.bottom * sy + 90
: fr.top + first.top * sy;
this.pdfSelDoc = doc;
this.openSelectionPopover({
mode: "create",
x: fr.left + first.left * sx + (first.width * sx) / 2,
y,
text,
cfi: "",
pdfPage: index,
pdfRects: rects,
});
},
// Trim a touch selection to the visible page. The visible column
// occupies content x [start - size, start]; endpoints beyond it are
// re-mapped to the page edge on their own line via caretRangeFromPoint.
clampSelectionToPage(sel: any, doc: any, r: any): boolean {
const left = r.start - r.size;
const right = r.start;
const TOL = 8;
const backwardProbe = doc.createRange();
backwardProbe.setStart(sel.anchorNode, sel.anchorOffset);
backwardProbe.setEnd(sel.focusNode, sel.focusOffset);
const backward = backwardProbe.collapsed;
const range = sel.getRangeAt(0);
const probeRect = (node: any, offset: number, forward: boolean) => {
const p = doc.createRange();
p.setStart(node, offset);
if (node?.nodeType === 3) {
if (forward && offset < node.length)
p.setEnd(node, offset + 1);
else if (!forward && offset > 0)
p.setStart(node, offset - 1);
}
if (p.collapsed) return null;
const rects = p.getClientRects();
return rects.length ? rects[forward ? 0 : rects.length - 1] : null;
};
const caretAt = (x: number, y: number) => {
const caret = doc.caretRangeFromPoint?.(x, y)
?? doc.caretPositionFromPoint?.(x, y);
if (!caret) return null;
return {
node: caret.startContainer ?? caret.offsetNode,
offset: caret.startOffset ?? caret.offset,
};
};
// selection end beyond the visible page (into the next column):
// trim to the last position on the visible page (bottom-right
// corner of the column — nearest-text mapping is unambiguous
// from the margin)
const endRect = probeRect(range.endContainer, range.endOffset, false);
if (endRect && endRect.x + endRect.width > right + TOL) {
const caret = caretAt(
right - 15, doc.documentElement.clientHeight - 40);
if (caret && range.isPointInRange(caret.node, caret.offset)) {
if (backward)
sel.setBaseAndExtent(caret.node, caret.offset,
sel.focusNode, sel.focusOffset);
else sel.extend(caret.node, caret.offset);
return true;
}
}
// selection start before the visible page (previous column):
// trim to the first position on the visible page (top-left corner)
const startRect = probeRect(range.startContainer, range.startOffset, true);
if (startRect && startRect.x < left - TOL) {
const caret = caretAt(left + 15, 40);
if (caret && range.isPointInRange(caret.node, caret.offset)) {
if (backward)
sel.extend(caret.node, caret.offset);
else
sel.setBaseAndExtent(caret.node, caret.offset,
sel.focusNode, sel.focusOffset);
return true;
}
}
return false;
},
openPopoverForSelection(sel: any, doc: any, index: number) {
const range = sel.getRangeAt(0);
const text = sel.toString().replace(/\s+/g, " ").trim();
if (!text) return;
let cfi: string;
let cfiEnd: string;
try {
cfi = this.view.getCFI(index, range);
// Collapse to the end point for a distinct end anchor —
// KOReader sync renders the highlight box from pos0/pos1, and
// pos1 == pos0 would be a degenerate (zero-length) range.
const endRange = range.cloneRange();
endRange.collapse(false);
cfiEnd = this.view.getCFI(index, endRange);
} catch {
return;
}
const frame = doc.defaultView?.frameElement as HTMLElement | null;
const iframeRect = frame?.getBoundingClientRect();
const rect = range.getBoundingClientRect();
// On touch the browser's own selection menu attaches ABOVE the
// selection (and paints over page content) — place ours BELOW it
// so both are visible. The popover anchors bottom-center at y-10
// and is ~44px tall; the drag handles occupy the first 24px below
// the selection, so the offset must clear them too.
const coarse = window.matchMedia("(pointer: coarse)").matches;
const y = coarse
? (iframeRect?.top ?? 0) + rect.bottom + 90
: (iframeRect?.top ?? 0) + rect.top;
this.openSelectionPopover({
mode: "create",
x: (iframeRect?.left ?? 0) + rect.left + rect.width / 2,
y,
text,
cfi,
cfiEnd,
});
if (coarse && this.view?.book?.dir !== "rtl")
this.showSelectionHandles(doc);
},
renderAllHighlights() {
for (const hl of this.highlightItems) {
if (hl.pdfPage >= 0) {
@@ -1332,7 +1625,12 @@ document.addEventListener("alpine:init", () => {
const rows = await hlResp.json();
this.highlightItems = (rows as any[])
.map((r) => this.mapHighlightRow(r))
.filter((hl: any) => hl.cfi || hl.pdfPage >= 0);
// Comic-treated items show no highlights (bookmarks only),
// and previously-created PDF highlights stop rendering.
.filter(
(hl: any) =>
!this.treatAsComic && (hl.cfi || hl.pdfPage >= 0),
);
this.renderAllHighlights();
}
if (noteResp.ok) {
@@ -1351,6 +1649,9 @@ document.addEventListener("alpine:init", () => {
const p = this.selectionPopover;
const token = getToken();
if (!token || !this.mediaItemId || (!p.cfi && p.pdfPage < 0)) return;
// The note editor writes into p.note live; "Highlight with note"
// creates the highlight AND its note in one POST.
const note = p.note.trim();
const pdfAnchor =
p.pdfPage >= 0
? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects })
@@ -1371,7 +1672,7 @@ document.addEventListener("alpine:init", () => {
epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi,
epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd,
color,
note_text: "",
note_text: note,
percentage_start: this.lastRelocateDetail?.fraction ?? 0,
}),
},
@@ -1400,16 +1701,23 @@ document.addEventListener("alpine:init", () => {
? ""
: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
color,
note: "",
note,
id: row.id,
});
// Clear the DOM selection: the gesture is consumed, and it is
// what keeps Chrome's native copy/share menu on screen.
try {
this.currentDoc?.getSelection()?.removeAllRanges();
} catch {
/* doc may be gone */
}
}
this.hideSelectionPopover();
} catch (_e) {
/* ignore highlight errors */
}
},
async saveHighlightChanges() {
async saveHighlightChanges(keepNoteOpen = false) {
const p = this.selectionPopover;
const token = getToken();
if (!token || !this.mediaItemId || !p.id) return;
@@ -1473,7 +1781,9 @@ document.addEventListener("alpine:init", () => {
id: p.id,
});
}
p.noteOpen = false;
// Explicit saves (the Save button) close the whole popover; color
// tweaks made while the note editor is open keep everything open.
if (!keepNoteOpen) this.hideSelectionPopover();
} catch (_e) {
/* ignore highlight errors */
}
@@ -1504,12 +1814,170 @@ document.addEventListener("alpine:init", () => {
}
},
async copySelectionText() {
const text = this.selectionPopover.text;
try {
await navigator.clipboard.writeText(this.selectionPopover.text);
this.hideSelectionPopover();
} catch (_e) {
/* clipboard unavailable */
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
this.finishCopy();
return;
}
} catch {
/* fall through to the legacy path */
}
// Plain HTTP (typical LAN access) has no navigator.clipboard;
// a transient textarea + execCommand works from this user
// gesture on insecure contexts.
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.append(ta);
ta.select();
document.execCommand("copy");
ta.remove();
this.finishCopy();
} catch {
/* ignore clipboard errors */
}
},
finishCopy() {
try {
this.currentDoc?.getSelection()?.removeAllRanges();
} catch {
/* doc may be gone */
}
this.hideSelectionPopover();
},
// ----- touch selection handles -----
// The native selection handles cannot be used (the custom gesture's
// touchstart preventDefault disables them along with the rest of the
// native controller), so adjustment after lift happens through these.
// Purely imperative DOM — position updates run on every drag frame.
selectionHandles: {
root: null as HTMLElement | null,
start: null as HTMLElement | null,
end: null as HTMLElement | null,
drag: null as "start" | "end" | null,
doc: null as any,
},
ensureSelectionHandles() {
const h = this.selectionHandles as any;
if (h.root) return;
const root = document.createElement("div");
root.id = "selection-handles";
const make = (which: "start" | "end") => {
const el = document.createElement("div");
el.className = "sel-handle";
el.addEventListener("pointerdown", (e: PointerEvent) => {
e.preventDefault();
e.stopPropagation();
h.drag = which;
el.setPointerCapture(e.pointerId);
});
el.addEventListener("pointermove", (e: PointerEvent) => {
if (h.drag !== which) return;
e.preventDefault();
this.dragSelectionHandle(e);
});
const release = () => {
h.drag = null;
};
el.addEventListener("pointerup", release);
el.addEventListener("pointercancel", release);
root.append(el);
return el;
};
h.start = make("start");
h.end = make("end");
h.root = root;
document.body.append(root);
},
showSelectionHandles(doc: any) {
this.ensureSelectionHandles();
const h = this.selectionHandles as any;
h.doc = doc;
h.root!.style.display = "block";
this.positionSelectionHandles(doc);
},
hideSelectionHandles() {
const h = this.selectionHandles as any;
if (!h.root) return;
h.root.style.display = "none";
h.drag = null;
},
// Host-space position of a selection boundary: the left edge of the
// start caret or the right edge of the end caret, at the line bottom.
caretHostPos(doc: any, node: any, offset: number, forward: boolean) {
try {
const p = doc.createRange();
p.setStart(node, offset);
if (node?.nodeType === 3) {
if (forward && offset < node.length) p.setEnd(node, offset + 1);
else if (!forward && offset > 0) p.setStart(node, offset - 1);
}
if (p.collapsed) return null;
const rects = p.getClientRects();
const r = rects[forward ? 0 : rects.length - 1];
if (!r) return null;
const off = doc.defaultView?.frameElement?.getBoundingClientRect();
return {
x: (off?.left ?? 0) + (forward ? r.left : r.right),
y: (off?.top ?? 0) + r.bottom,
};
} catch {
return null;
}
},
positionSelectionHandles(doc: any) {
const h = this.selectionHandles as any;
if (!h.root || h.root.style.display === "none") return;
const sel = doc.getSelection();
if (!sel?.rangeCount || sel.isCollapsed) {
this.hideSelectionHandles();
return;
}
const range = sel.getRangeAt(0);
const startPos = this.caretHostPos(doc, range.startContainer,
range.startOffset, true);
const endPos = this.caretHostPos(doc, range.endContainer,
range.endOffset, false);
if (!startPos || !endPos) {
this.hideSelectionHandles();
return;
}
h.start!.style.transform = `translate(${startPos.x - 6}px, ${startPos.y}px)`;
h.end!.style.transform = `translate(${endPos.x - 6}px, ${endPos.y}px)`;
},
dragSelectionHandle(ev: PointerEvent) {
const h = this.selectionHandles as any;
const doc = h.doc;
const r: any = this.renderer;
if (!doc || !r || r.scrolled) return;
if (this.view?.book?.dir === "rtl") return;
const frame = doc.defaultView?.frameElement;
if (!frame) return;
const off = frame.getBoundingClientRect();
// pointer → iframe content space, clamped to the visible page
const size = r.size;
const left = r.start - size;
const right = r.start;
const x = Math.min(Math.max(ev.clientX - off.left, left + 2), right - 2);
const y = ev.clientY - off.top;
const caret = doc.caretRangeFromPoint?.(x, y)
?? doc.caretPositionFromPoint?.(x, y);
if (!caret) return;
const node = caret.startContainer ?? caret.offsetNode;
const offset = caret.startOffset ?? caret.offset;
const sel = doc.getSelection();
if (!sel?.rangeCount || !node) return;
const range = sel.getRangeAt(0);
if (h.drag === "start")
sel.setBaseAndExtent(node, offset, range.endContainer, range.endOffset);
else
sel.setBaseAndExtent(range.startContainer, range.startOffset,
node, offset);
this.positionSelectionHandles(doc);
},
goToHighlight(hl: {
cfi: string;
@@ -1698,6 +2166,8 @@ document.addEventListener("alpine:init", () => {
},
setInteractionMode(mode: string) {
if (!this.isFixedLayout || !this.isPDF) return;
// Comic-treated PDFs have no text selection: pan only.
if (this.treatAsComic) return;
if (mode !== "select" && mode !== "pan" && mode !== "text") return;
this.renderer.setAttribute("interaction-mode", mode);
this.interactionMode = mode;
@@ -1792,8 +2262,9 @@ document.addEventListener("alpine:init", () => {
},
toggleSearch() {
// Reflowable books use foliate's DOM search; PDFs use extracted text.
// Comics have no text at all.
if (this.isFixedLayout && !this.isPDF) return;
// Comics have no text at all — and comic-treated PDFs (manga scans)
// opt out of search to match comic treatment: bookmarks only.
if ((this.isFixedLayout && !this.isPDF) || this.treatAsComic) return;
const opening = !this.searchOpen;
this.closeDrawers();
this.searchOpen = opening;
@@ -2201,8 +2672,8 @@ document.addEventListener("alpine:init", () => {
this.hyphenate = true;
this.chromeBehavior = "auto-hide";
this.applyChromeBehavior();
this.interactionMode = "select";
this.setInteractionMode("select");
this.interactionMode = this.treatAsComic ? "pan" : "select";
this.setInteractionMode(this.interactionMode);
this.fxBrightness = 1;
this.fxContrast = 1;
this.fxInvert = false;
+1 -1
View File
File diff suppressed because one or more lines are too long
+31
View File
@@ -1434,3 +1434,34 @@
}
}
}
/* Touch selection handles (custom gesture — the native ones are disabled) */
#selection-handles {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 55;
}
#selection-handles .sel-handle {
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 24px;
pointer-events: auto;
touch-action: none;
background: #8ab4f8;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
opacity: 0.95;
}
#selection-handles .sel-handle::before {
content: "";
position: absolute;
top: -4px;
left: 50%;
width: 2px;
height: 5px;
background: inherit;
transform: translateX(-50%);
}
+1 -1
View File
File diff suppressed because one or more lines are too long