Commit Graph
306 Commits
Author SHA1 Message Date
john-okeefe 24ea9d8a38 feat(reader): touch & mobile — tap zones, gesture engine, mobile sheets
Phase 2 of the reader redesign:

- Fixed-layout touch engine (foliate-js e9e61d8): pinch-zoom around
  the midpoint, two-finger pan, single-finger pan while zoomed,
  horizontal swipe page-turn at fit (RTL-aware via next()/prev()),
  and double-tap to zoom 2.5x / reset. Touch events forwarded from
  page iframes with converted coordinates; preventDefault only when
  the engine consumes the gesture, so PDF text selection and native
  taps stay intact. touch-action: none on the host and in comic/pdf
  page documents keeps the browser from fighting the engine.
- Tap zones (Kindle-style) for touch devices: tap the outer margins
  to page, center to toggle chrome. Size configurable (10-50%) via
  the revived tap_zone_size setting; toggle via new tap_zones_enabled
  (Behavior section of the settings drawer). Pointer-based + passive
  so drags/swipes/selection never trigger; attached both to the
  viewport and inside every page document (iframe events don't
  bubble); debounced 280ms so double-tap zoom doesn't also page; no
  zone actions while a fixed-layout page is zoomed.
- Drawers become full-width sheets on screens <= 640px.
2026-08-14 16:00:19 -04:00
john-okeefe 14d1a158a0 feat(reader): glass chrome — translucent bars, custom slider, slide-away hide
Modernize the reader chrome bars without touching the drawer system:

- Bars become theme-tinted glass: 70% bg-primary translucency over
  the edge-to-edge page, 18px backdrop blur + saturation, hairline
  translucent borders, soft directional shadows (single .reader-glass
  class owns the effect; replaces solid opaque backgrounds and the
  tailwind backdrop-blur that would override it).
- Chrome hide/show now slides the bars off-screen (translateY) in
  addition to the opacity fade, via .chrome-hidden on #reader-chrome.
- Theme-aware hover pills (translucent currentColor tint) replace
  hard-coded gray-700 hovers; focus-visible rings added.
- Progress slider gets a custom thin rounded track with a floating
  white thumb (webkit + gecko), replacing native range styling.
- Separators and the fit-mode select match the glass language
  (.reader-sep, .reader-select).
2026-08-14 15:16:05 -04:00
john-okeefe 612f888683 feat(reader): immersive chrome, slide-over drawers, tri-state PDF pointer mode
Phase 1 of the reader redesign:

- Reading surface is edge-to-edge; top/bottom bars overlay
  translucently (backdrop-blur) instead of reserving insets, killing
  the inset-coordination bug class entirely. Chrome auto-hides after
  2.5s of pointer inactivity (chrome_behavior setting finally wired:
  auto-hide / always-visible; legacy values map to auto-hide). Pointer
  activity inside page iframes keeps it awake; Esc toggles.
- TOC / Settings / Bookmarks become slide-over drawers with a scrim
  (z-50, full-height, safe-area aware), replacing the dockable-panel
  system and its window-shade headers. Only one drawer opens at a
  time; Esc or scrim click closes.
- Bottom bar is contextual: reflowable keeps nav/slider/progress/TOC;
  fixed-layout row adds Fit Page/Width select, zoom cluster,
  magnifier (now shows active state), Double Page Spread toggle, and
  a Smart | Pan | Text segmented control replacing the cryptic
  two-state icon. Smart = text-aware drag; Text = selection-only
  (manual smart-detect off); Pan = force pan. Choice persists via
  pdf_interaction_mode (new setting + foliate 29bc958 'text' mode).
- Settings drawer: Behavior (chrome, progress mode), Appearance with
  18 Kindle-style theme swatches (single source of truth from
  THEME_COLORS), Typography, Layout — each scoped by format.
- Keyboard: t/s/b open TOC/settings/bookmark, Esc closes drawers
  before toggling chrome, shortcuts skip form inputs; both slider
  rows tracked correctly (no duplicate-ID lookups).
- Topbar: Back, title, add-bookmark, bookmarks drawer, Aa settings;
  chrome follows user theme.
2026-08-14 14:59:51 -04:00
john-okeefe ba95cc3e8b fix(reader): stabilize chrome panels, bookmarks end-to-end, dead UI removal
Phase 0 of the reader redesign:

- Panels no longer render under the top/bottom bars: sidebars get
  measured insets (same resize/safe-area mechanism as the viewport);
  panel max-height now derives from the bounded sidebar instead of a
  100vh guess; right-side border targets the actual sidebar.
- Bookmarks work end-to-end for the first time: REST CRUD under
  /api/media-items/:id/bookmarks (create/delete route through
  AnnotationService for dedup/LWW/tombstones), fix UpdateMediaBookmark
  referencing nonexistent updated_at column, frontend posts to the
  real API with per-format position (CFI vs page), live list with
  jump + delete instead of SSR-only snapshot.
- Fix chapter matching in progress saves: boundaries were compared by
  a nonexistent tocItem property, so chapter was never persisted.
- Remove dead UI: Navigator panel stub, empty dictionary popup shell,
  unwired Chrome Behavior select; purge 160 stale build artifacts.
- Reader chrome now follows the user's app theme instead of hardcoded
  theme-tokyo-night.
2026-08-14 09:05:33 -04:00
john-okeefe 8004cb81a5 feat(ui): admin Hash Conflicts page and nav entry
New /admin/hash-conflicts page (admin-only) listing pending
content-duplicate groups. Each group card shows the library, a
shortened SHA-256, and one row per copy with title, author, path,
size, and per-copy reading-data counts (progress, highlights,
bookmarks, notes, collections) - copies that own user data are
highlighted so the keep choice is informed.

Per copy: 'Keep this copy' merges the other copies' child rows into
it and deletes them. Per group: 'Keep both' for intentional
duplicates. Both confirm first, resolve via htmx POST, and swap the
card for a resolved confirmation inline. The confirmation fragment is
built inline in the handler rather than the templates package
(templates imports handlers; a back-import would be a cycle).

Empty state shown when no conflicts are pending. Adds a 'Hash
Conflicts' entry to the admin sidebar section between Libraries and
Users.
2026-08-14 08:52:33 -04:00
john-okeefe f5d9578375 feat(reader): wire double-page spread setting into web reader
The double_page_spread checkbox in the reader settings panel was inert:
it had no Alpine binding, no apply logic, and no persistence. Default
was also inconsistent (false in settings-manager, absent from server
defaults).

- Add doublePageSpread state to the reader Alpine component, loaded
  from saved settings (default true)
- Add applyDoublePageSpread() which sets the renderer's 'spread'
  attribute to auto/none and persists the setting via saveSettings
- Apply the spread attribute during fixed-layout renderer init
- Bind the settings checkbox with x-model and @change
- Add double_page_spread: true to ReaderService server defaults so
  new users get the same starting value the client expects
- Also improve the PDF pan/select toolbar button: distinct smart-
  select vs pan icons, highlighted state while pan mode is active,
  and dynamic tooltips/aria-labels explaining each mode
2026-08-14 08:17:25 -04:00
john-okeefe 04e2a069d6 fix(dashboard): stop duplicating items after scan completion
Release / build-and-push (push) Successful in 5m9s
The scan-complete handler in dashboard.ts attempted to deduplicate book
cards by querying [data-media-item-id], but neither the client-side
renderBookCard nor the server-side BookCard template ever set that
attribute. As a result the dedup Set was always empty, every item from
the API response was treated as new, and all items were prepended via
insertAdjacentHTML('afterbegin', ...) on every 5-minute scan — causing
visible duplication (doubling, tripling) that only cleared on page
refresh.

Fix by replacing the fragile dedup-and-prepend logic with a per-track
full innerHTML replace. This is simpler, correctly handles items that
should be removed after a scan (the old code never removed anything),
and also removes stale sections no longer returned by the API.

Additional hardening:
- Add data-media-item-id to both renderBookCard (dashboard.ts) and the
  server-side card wrapper (dashboard.templ) so server-rendered and
  JS-rendered cards are structurally identical.
- Guard the bookhoard:scan-complete listener registration with a
  module-level boolean (scanListenerRegistered) so the handler cannot
  accumulate if Alpine ever re-inits the body subtree.
- Remove debug console.log statements from the scan handler.
2026-08-10 14:46:13 -04:00
john-okeefe 05c7431d86 feat(ui): accordion sidebar panels + wood texture preview swatches
Release / build-and-push (push) Canceled after 42s
Sidebar appearance menu improvements:

Accordion behavior:
- Lift panel open/close state to a shared 'openPanel' variable on the
  parent container so only one sidebar panel (User, Appearance, Admin,
  Sign In) can be open at a time; all can be closed.
- Admin panel still auto-opens on /admin/* pages via initial state.
- Add chevron rotation to User and Appearance panels (previously only
  Admin rotated); add a chevron to the Sign In panel for consistency.

Wood texture previews:
- Generate 48x48 WebP thumbnails (~200 bytes each) from the full-size
  PNG textures (873 KB – 1.9 MB) so the bookshelf option circles show
  the actual wood grain instead of a flat grey dot.
- Use unquoted url() in the inline style to avoid templ's double-HTML
  escaping of single quotes (SanitizeStyleAttributeValues + EscapeString
  turned url('...') into url(&#39;...) which is invalid CSS).
- 'None' keeps the flat neutral circle.

CSS resilience:
- Move the wood background-image: url() rules from the compiled
  style.css into input.css (the Tailwind source) so they survive CSS
  rebuilds instead of being silently lost.
2026-08-10 14:22:46 -04:00
john-okeefe 9a62c10803 Merge branch 'new-ui'
Release / build-and-push (push) Successful in 2m44s
# Conflicts:
#	templates/admin_library.templ
#	templates/admin_library_templ.go
#	templates/conflicts_templ.go
#	templates/unlinked_books_templ.go
2026-08-10 13:48:06 -04:00
john-okeefe 8d65e03555 fix(ui): move theme checkmark when theme changes in Appearance menu
The checkmark in the Appearance theme menu was rendered server-side
(if user.Theme == opt.Name), so it never moved after switching themes
in the browser.

- Always render the check for every theme option, hidden by default,
  using a new themeCheckClass(name, current) helper that returns the
  hidden class unless the option is the active theme.
- Give each theme button a data-theme attribute and add
  updateThemeIndicators() to web/src/theme.ts, which reads the applied
  theme from the body class (theme-<name>) and toggles the hidden class
  on each check accordingly.
- Call updateThemeIndicators() from changeTheme (before the async save
  and on failure), initializeTheme, and loadUserTheme so the menu stays
  in sync with the applied theme.
- Add unit test for themeCheckClass (templates/utils_test.go) and
  regenerate templ output.
2026-08-10 13:43:32 -04:00
john-okeefe ab465d8e0e feat(ui): adopt book-open brand icon and add SVG favicon
Bring the tighten-ui brand treatment into new-ui:

- Replace the emoji book (📚) in the sidebar header with the book-open
  icon rendered in the theme accent color, matching the tighten-ui
  header brand (templates/header.templ).
- Add web/static/favicon.svg (book-open glyph, tokyo-night accent
  #7aa2f7 stroke) and reference it from the <head> of all 27 page
  templates, so the favicon is present on login/setup/error pages too.
- Regenerate templ output for all affected templates.
2026-08-10 13:43:13 -04:00
john-okeefe 598d70f735 feat(admin): editable tunable settings UI with grouped sub-sections
Replace the read-only "System Information" card (which listed hardcoded
values) with editable HTMX forms, organized so the live vs restart
distinction and related settings are visually clear.

admin_settings.templ:
- AdminSettings signature now takes liveGroups and restartGroups
  ([]SettingGroup) instead of a flat entry list.
- Remove the static System Information list. Render two cards: "Live"
  (green, applies immediately) and "Restart Required" (warning header,
  saved but only takes effect after restart).
- Within each card, TunableSettingsSection clusters entries into
  labeled sub-sections by Group (e.g. "Password Quality", "Device Rate
  Limits", "Login Lockout", "Worker Pool") with uppercase tracked
  sub-headers.
- TunableSettingRow renders an inline HTMX form per setting: a Yes/No
  select for bools, a number input with min/max for ints, text
  otherwise, posting to /admin/settings/tunable. Rows show "modified
  from default" when the value differs from the compiled default.

types.go:
- Add SettingEntry (template-local mirror of database.SettingEntry,
  keeps templates from importing database) and SettingGroup.

utils.go:
- Add GroupTunableSettings: splits a flat, group-sorted entry list into
  live and restart []SettingGroup buckets preserving source order.
  utils_test.go covers the multi-group + empty cases.

frontend.go:
- The /admin/settings page handler now loads entries from the registry,
  drops the three keys that have dedicated UI cards (default_timezone
  dropdown, scan_poll_interval_seconds, auto_scan_enabled) so they are
  not listed twice, groups the rest, and passes liveGroups/restartGroups
  into the template.
2026-08-10 08:03:02 -04:00
john-okeefe 89ea310414 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	templates/book_detail.templ
#	templates/collection_rules.templ
#	templates/conflicts.templ
#	templates/header.templ
#	templates/progress.templ
2026-08-08 15:43:16 -04:00
john-okeefe 4b152bbe4e fix(templates): use expression attributes and fix indentation
Convert string-interpolation attributes (value="{ x }") to templ
expression attributes (value={ x }) for IDs, paths, and titles, and
fix indentation in header.templ and progress.templ.
2026-08-08 15:40:15 -04:00
john-okeefe 64ad10b3f2 feat(admin): add scan settings + system information to settings page
- Scanning section: auto-scan toggle (on/off) and poll interval input
  with HTMX form that updates system_settings table
- System Information section: surfaces all hardcoded constants
  (session duration, password policy, rate limits, worker pool,
  sync queue, tombstone TTL, OPDS page size, CORS, etc.)
- New ScanSettingsData type, ScanSettingsSection partial template
- New HTMX endpoint: PUT /admin/settings/scan
- registerAdminSettingsRoutes for scan settings HTMX CRUD
- Fix bookhoord→bookhoard import typo in admin_library.go
2026-08-07 23:37:14 -04:00
john-okeefe a42232e67a fix(admin): expandable sidebar section + library manage toggle
- Admin section in sidebar now uses same expandable panel pattern as
  Appearance and User sections (toggle button with chevron, auto-opens
  when on /admin pages)
- Library Manage button toggles open/close instead of only opening
  (uses htmx.ajax for open, clears panel for close)
2026-08-07 23:29:01 -04:00
john-okeefe 706be09dec feat(admin): redesign library management with HTMX expandable rows + stats dashboard
Library management:
- Redesign admin_library page with expandable rows that load detail
  panels via HTMX (LibraryList, LibraryPanel, FolderBrowserContent partials)
- Add create/edit/delete library modals using data-* attributes
- Add folder browser modal with inline add/remove via HTMX
- Add user visibility checkboxes toggled via HTMX
- New endpoints in admin_library.go: create, update, delete, panel,
  folders add/remove, browse, visibility
- New template types: FolderData, DirEntry, UserVisibilityData,
  AdminStats; extend LibraryData with TypeValue and FolderCount

Admin dashboard:
- Rewrite admin.templ to show 4-stat grid (libraries, media, users, devices)
- Add getAdminStats helper querying library/user/media/device counts
- Pass AdminStats to template from both /admin and /admin/ handlers
2026-08-07 09:35:39 -04:00
john-okeefe a87c8afc22 refactor(admin): remove separate sidebar, integrate admin nav into main sidebar
- Delete AdminSidebar component entirely
- Add Administration section (Dashboard, Libraries, Users, Settings) to
  header sidebar, visible only for admin users
- Remove Admin Panel link from user dropdown
- Strip admin chrome (sidebar wrapper, back buttons) from all 5 admin
  page templates
- Fix activeClass to handle trailing-slash routes correctly
- Add isUserVisible helper for library visibility toggles
- Fix processing issues page: remove dead Alpine JS, wire HTMX dismiss
  with proper mediaItemId, add issue ID swap targets
- Add processing issue resolve/delete routes to library router
- Add GetProcessingIssueStatsData context-based method
- Fix users page: remove broken hx-headers auth, simplify role select
- Fix settings page: remove dead adminSettings Alpine ref
2026-08-07 09:35:30 -04:00
john-okeefe e09c55cecc feat(devices): reform Add Device modal with KOReader plugin instructions
- Add gear icon (proper cog) and use it for device settings button
- Replace KOReader manual identifier input with step-by-step plugin
  setup instructions (clone repo, enter server URL, approve pending reg)
- Show server URL with copy button pre-filled from baseURL
- Kobo keeps manual registration flow (device name + identifier)
- Comment out Web Browser and Mobile App options (not implemented)
- Use Alpine x-model on device-type select to toggle between
  KOReader instructions and Kobo registration form
2026-08-06 14:48:48 -04:00
john-okeefe 0a0caa9533 fix(ui): round progress percentage to 2 decimals on progress page
The large percentage badge displayed the raw float value unformatted
(e.g. 1.523456789%). Now uses %.2f for a clean display (e.g. 1.52%).
2026-08-06 13:25:54 -04:00
john-okeefe 8e2c1a4b3a fix(ui): make icon and color selection work in collection modal
Three root causes, all fixed:

1. Icon buttons were created with setAttribute('onclick', ...) in
   populateIconGrid, but selectIcon is module-scoped (not on window),
   so clicking threw ReferenceError. Switch to addEventListener with
   a closure. Icon search/focus used plain oninput/onfocus attributes
   with the same problem — convert to Alpine @input/@focus.

2. selectColor's highlight selector queried [onclick="selectColor(...)\]
2026-08-06 11:35:14 -04:00
john-okeefe 4a870a7f18 fix(ui): keep search filter visible on system collections 2026-08-06 11:22:26 -04:00
john-okeefe 40dabfd788 feat(ui): hide management controls for system collections
System collections (Not Started, Continue Reading, etc.) compute
their contents dynamically from reading_progress, so manual
add/remove has no effect. Hide the search bar, Remove Selected
button, Add Books button, per-card checkboxes, and Remove buttons
when the collection is system, making the page visually read-only.

- Add IsSystem bool to CollectionData, populated from the database
  is_system_collection flag.
- Add data-is-system to #collection-data so the JS renderer can
  also conditionally omit controls on library switch.
- Wrap toolbar controls, book picker modal, card checkboxes, and
  remove buttons in if !collection.IsSystem in the template.
2026-08-06 11:08:29 -04:00
john-okeefe ef3c05714a fix(ui): custom confirm dialog, picker visibility, remove wiring
Three fixes for the collection detail page:

1. Picker modal never showed because the outer overlay div had
   style="display:none" with no x-show binding — add x-show bound
   to $store.bookPicker.isOpen plus a backdrop and click-to-close.

2. Replace native confirm() with an in-page Alpine modal for UI
   continuity. Add confirm dialog state (showConfirm, confirmMessage,
   pendingAction) and methods (requestRemoveBook, requestBulkRemove,
   executeConfirmed, closeConfirm) to the collections component.
   The actual API calls (doRemoveBook/doBulkRemove) are triggered only
   when the user confirms.

3. Rename removeBook → requestRemoveBook and bulkRemove →
   requestBulkRemove in template + JS renderer so the dialog opens
   instead of navigating or failing silently.
2026-08-06 10:55:37 -04:00
john-okeefe 816ee0ec80 fix(ui): wire up collection detail page interactions
The /collections/:id page had several broken features because three
referenced functions (removeBook, toggleBookForRemoval,
filterCollectionBooks) were never defined, and every book card was
wrapped in <a href="/media/..."> so clicking the checkbox or remove
button navigated to the book detail page instead.

Card restructure:
- Remove the <a> wrapper; title and cover are now individual links.
- Checkbox sits in a <label> with expanded click area (p-2 -m-2).
- Checkbox uses Alpine :checked/@change bound to a reactive
  selectedBooks array on the collections component.

Remove (single + bulk):
- Add removeBook(id) and bulkRemove() methods with confirm() dialogs.
- Wire the "Remove Selected" button with :disabled binding and @click.
- Selected-count badge is now Alpine-reactive (x-show/x-text).

Search within collection:
- Add filterCollectionBooks() that filters cards client-side by
  title/author via data-* attributes and @input.

Book picker ("Add Books"):
- Point the HTMX search inputs at the existing /api/media-items/search
  endpoint instead of the non-existent /api/media-items/filtered.
- Add hx-trigger="loadBooks" + hx-get to the grid so loadBooks()
  actually fires an initial request when the picker opens.
- Merge the hidden limit/offset inputs into the #book-picker-filters
  div so hx-include picks them up (was a separate <form id=filter-form>
  that nobody referenced).
- Add show_checkbox mode to handleSearchHTML: when present, render a
  new BookPickerGrid template with clickable, selectable cards instead
  of the reader BookCard.
- Fix bookPicker submit() to location.reload() instead of a non-existent
  reloadCollection HTMX event, and clearFilters() to target text inputs.
2026-08-06 10:40:18 -04:00
john-okeefe 73a2852ee3 fix(reader): back button remembers the page you came from
The reader's back button was hardcoded to the book detail page
(/media/{id}), so even when you launched the reader straight from the
dashboard the back button ignored that and sent you to the detail view.

Mirror the existing book-detail.ts referrer pattern: capture
document.referrer into sessionStorage on load (excluding other reader
pages and the reader's own URL), then override the back link's click to
navigate there. Falls back to the link's original href (/media/{id}) when
no valid referrer exists (direct URL access).
2026-08-06 09:51:04 -04:00
john-okeefe 7b465ac97e fix(ui): use a grid icon for Customize Dashboard
The settings icon is a circle with radiating spokes, which reads as a
sun/light-mode toggle rather than a dashboard control. Swap it for the
grid icon, the standard dashboard-layout affordance.
2026-08-06 08:24:54 -04:00
john-okeefe 033a012069 feat(ui): group sidebar footer menus into distinct panels
The account and appearance menus sat in one container with no separation,
so expanding one made the other hard to find. Each collapsible menu now
lives in its own subtly lifted, bordered panel (.sidebar-panel) so the
expanded items stay visually contained and the menus never blend together.
Applied to the account, appearance, and sign-in menus.
2026-08-06 08:14:10 -04:00
john-okeefe 9165c4eb3f feat(ui): use a book-open icon for the card read action
Swaps the play (triangle) icon for book-open on book cards, since the
action is to start reading rather than play media.
2026-08-06 08:01:33 -04:00
john-okeefe 9ef6c5b6ed feat(ui): split book-card play action into reader/detail routing
The play button on book cards now opens the reader directly, instead of
always going to the detail page. Cards with an active progress sync
conflict route the play button to the detail page (which hosts the
conflict dialogue and resolves before writing progress), so the user is
never silently dropped into the reader with an unresolved conflict.

Backend:
- Add HasConflict to BookInfo and stamp it via ListSyncConflictsByUser
  (MarkActiveConflicts / MarkActiveConflictsSections) on the dashboard,
  bookshelf, series, tag, and search result card builders.
- Each page issues a single conflict query regardless of card count.

BookCard:
- Restructure into a detail link (cover + meta) with the play action as a
  sibling overlay using a pointer-events split: the container passes
  clicks through to detail while only the circular button routes to the
  reader. No nested anchors.
- On touch devices (hover: none) the play button stays visible.

Fix: carousel nav buttons had opacity-0 without pointer-events-none, so
they swallowed hover/clicks over book cards on the dashboard. They are
now click-through until the carousel is hovered.
2026-08-06 07:52:20 -04:00
john-okeefe 987ece38f0 fix(reader): keep footer compact on mobile and clear text under chrome
On phones the footer's progress cell rendered the full chapter title (e.g. 'Long Chapter Name · 5 / 12') in a div with no max-width or nowrap, so the text wrapped to multiple lines and ballooned the bottom bar. Combined with viewport offsets that were computed from assumed pixel heights with ~0px margin, the book text slipped underneath the bars.

Chapter label in footer: split #progress-display into two spans (progressLabel hidden on mobile via 'hidden sm:inline', progressMain always shown) and cap it with 'truncate whitespace-nowrap max-w-[5rem] sm:max-w-none' so it can never wrap or grow the bar. Phones now show just '5 / 12'; larger screens keep 'Chapter · 5 / 12'.

Viewport offset: replace the fragile hardcoded calc() with runtime measurement. Gave the chrome bars ids (reader-topbar/reader-bottombar) and added updateViewportInsets(), which sets #reader-viewport top/bottom from each bar's real offsetHeight (which already includes env(safe-area-inset-*) padding) plus a 6px margin. It runs on init and refreshes on resize, orientationchange, and via a ResizeObserver, so the content area tracks the actual chrome height on any DPI, notch, home-indicator, or zoom level instead of guessing.

Refactored formatProgress into formatProgressParts (returns {label, main}; only chapter mode sets a label) with a setProgress() helper wiring progressLabel/progressMain/progressText across the relocate, cycleProgressMode, and applyProgressMode call sites. Rebuilt reader_templ.go and style.css.
2026-08-05 21:58:42 -04:00
john-okeefe 8ea70ea7f8 feat(ui): migrate remaining pages to the new shell
Apply the sidebar shell and bold primitives across the rest of the app so
the whole experience shares one visual language:

- Browse/organize: series (keeps stacked-covers/series-card CSS),
  collections (keeps wood-paneling + carousel classes), browse_detail.
- Account/stats: profile + form + modal, progress, analytics (stat-card
  tiles), devices, conflicts (status semantics), queue (status/priority
  badges).
- Admin: admin sidebar restyled with icons, dashboard/users/library/
  processing-issues/settings migrated to .card/.btn/.input primitives.
- Docs/setup/misc: docs (keeps prose/highlight.js), api_explorer, setup
  wizard, unlinked_books, custom_section builder.
- Modals/fragments: collection_modal, collection_rules,
  restore_system_collection_modal, filter_item, book_detail_modals — all
  overlays use --surface-overlay + --shadow-pop.

Every Alpine handler, HTMX attribute, id, name, and data-* is preserved;
only presentation changes.
2026-08-05 16:45:02 -04:00
john-okeefe 07578e0206 feat(ui): redesign dashboard, bookshelf, book detail, and auth pages
- Dashboard: page heading, bold section headers with accent icon tiles,
  carousel chevrons as SVG icons with theme-aware gradients (wood-paneling
  gradient classes preserved), and a cinematic BookCard (hover overlay with
  a quick-action button). BookCard is now fluid so it fills both the
  carousel slot and the bookshelf grid.
- Bookshelf: the filter wall becomes a search toolbar + a slide-in filter
  drawer (filtersOpen state added to the bookshelf Alpine component). Every
  filter input, the tristate cover toggle, tag autocomplete, save/load/clear
  actions, HTMX search/sort, and pagination are preserved.
- Book detail: blurred cover backdrop hero, rounded-2xl cover, bold
  typography, .btn action bar, progress/metadata cards, chip-style external
  links. All interactive rating, modals, and data-attrs preserved.
- Auth/landing: brand-gradient hero for index/login/register, icon feature
  cards, data-driven theme select (ThemeOptions). All ids (#theme-select,
  #result, #auth-result, password-requirement ids) and Alpine init preserved.
2026-08-05 16:44:52 -04:00
john-okeefe f402a3ee03 feat(ui): sidebar app shell and bold design system
Replace the top-nav with a fixed left sidebar + slim content topbar
(the Komga/Audiobookshelf layout), the signature change versus the
conservative tighten-ui branch.

- App shell: .app-sidebar (off-canvas on mobile via Alpine mobileMenuOpen,
  pinned at 16rem on lg), .app-topbar (fixed, blurred, 4rem), and
  .app-subbar for in-page sticky bars (parks under the topbar). Content is
  auto-offset via body:has(.app-sidebar) so reader.templ/error.templ (which
  have no sidebar) are untouched.
- Header rebuilt as the sidebar: logo + vertical nav (activeClass), an
  inline Appearance picker driven by ThemeOptions/WoodOptions, and an
  inline account menu / sign-in (preserving the inline htmx login). The
  search lives in the topbar so its dropdown still anchors correctly.
- Bolder primitives: .card -> rounded-2xl, cinematic .book-card-cover
  hover overlay with a quick-action affordance, .brand-gradient hero
  surface, .hero-backdrop (blurred cover) and .stat-card utilities.
2026-08-05 16:44:42 -04:00
john-okeefe f3f908eb7e feat(ui): design system foundation
Add semantic design tokens and base primitives to replace the verbose
inline var() styling that made the UI feel dated.

- Rename colliding Tailwind color tokens (bg-primary/bg-secondary) to
  semantic names (surface/surface-raised/content/content-muted/brand/line)
- Derive hover, overlay, border-strong, accent-muted and elevation tokens
  once on <body> so they adapt to every theme automatically
- Make :root mirror Tokyo Night to kill the first-paint theme flash
- Add component primitives in @layer components: .card (surface + soft
  shadow, no hard border), .btn variants, .input, .chip, .badge, .icon-btn
- Add theme-aware status/priority badges and global focus-visible styling
- Add an inline-SVG Icon() component (consistent stroke language) to
  replace the mixed emoji/SVG iconography
- Data-drive theme/wood options and add an active-nav helper
- Add skeleton shimmer + x-cloak support
2026-08-05 16:03:07 -04:00
john-okeefe 11617c1860 fix(reader): tighten mobile header/footer and add safe-area margins
The reader chrome used the same dimensions at every screen size, and the book viewport offset was hardcoded to 52px. This made the header/footer oversized on phones and left the body text flush against (or overlapping) the bars, with no handling for notched-device safe areas.

- Add viewport-fit=cover so notched devices expose safe-area insets.
- Shrink the top bar on mobile (px-3 py-2 / text-base, scaling up at sm:) and hide the chapter title on phones (hidden sm:block sm:truncate).
- Shrink the bottom bar on mobile (tighter padding/gap, p-1.5 sm:p-2 on buttons) while keeping all controls visible.
- Replace the hardcoded top-[52px] bottom-[52px] viewport offsets with responsive calc() values (44/48px mobile, 60px at sm:) that fold in env(safe-area-inset-*), plus matching safe-area padding on the bars, so the book content always clears the chrome with a visible margin.

Regenerates reader_templ.go and rebuilds style.css.
2026-08-05 15:31:40 -04:00
john-okeefe bf83492bf7 feat(book-detail): add Mark as Read / Unread toggle button
The book detail page had no way to mark a book finished or reset its
read state from the UI. Reading state is modelled by reading_progress
alone, where 'read' is the canonical signal percentage >= 1.0 (used by
the dashboard Recently Read collection, analytics, and sync priority).

Add a single toggle button in the action row (after Read Now) whose
label is server-rendered from completion state:
- not read  -> "Mark as Read"    -> PUT /api/media-items/:id/progress
                                       { percentage: 1.0 }
- read      -> "Mark as Unread"  -> DELETE /api/media-items/:id/progress

Mark as Unread cannot use PUT { percentage: 0 }: the progress handler
silently ignores percentage < 0.005 when existing progress > 0.01
(internal/handlers/media.go anti-regression guard), so DELETE is the
only reliable reset.

If the book has an active sync mismatch (an unresolved sync_conflicts
row), the toggle resolves it first via POST /api/conflicts/:id/resolve
before writing progress. Order matters: resolving sets resolved_at,
arming the 10-minute HasRecentConflictResolution suppression window so
the subsequent progress write does not spawn a brand-new conflict. The
resolve winner is any valid source key from the conflict data (prefers
"web"); it does not affect the final state, which the progress write
sets. A 400 "already resolved" response is tolerated.

Notes, highlights, and ratings are independent of reading_progress (they
reference media_items, not progress) and are never affected by the
toggle. After toggling the page reloads so the progress card, Sync
Progress button, and conflict banner re-render server-side.

- templates/utils.go: add conflictWinnerSource and conflictID helpers.
- templates/book_detail.templ: data-conflict-id/winner on <body> and the
  toggle button.
- web/src/book-detail.ts: toggleRead() + conflictId/conflictWinner/
  readSaving state (read from <body> in init()).
- templates/book_detail_templ.go regenerated.
2026-07-30 13:31:18 -04:00
john-okeefe 33c69e7c71 chore(templates): regenerate stale book_detail_modals templ output
Running `templ generate` to pick up the book_detail changes also
resynced book_detail_modals_templ.go, whose committed output was stale
relative to its source. The regeneration (templ v0.3.1020) reformats
boolean attribute rendering (e.g. `selected`) via
templ.ResolveAttributeValue and reflects pre-existing source additions
such as id/for label associations.

No source (.templ) change in this file; generated output only.
2026-07-30 13:08:25 -04:00
john-okeefe ca8c592496 feat(book-detail): add interactive half-star rating widget
The book detail page only displayed user ratings as static, non-clickable
stars. The full rating CRUD stack already existed in the backend
(media_ratings table, POST/GET/PUT/DELETE /api/media-items/:id/rating)
but nothing in the web UI could create or update a rating.

Replace the display-only renderStars output for the user rating with an
Alpine.js widget that:
- Renders 5 stars, each split into two transparent hit zones so the
  underlying 1-10 scale maps to half-star precision (left half = x.5,
  right half = whole star).
- Shows a live hover preview via a ratingHover state field.
- Saves the rating in place through POST /api/media-items/:id/rating
  (which upserts) and reflects the value immediately, with no full page
  reload.
- Displays the numeric value (e.g. "3.5 / 5") and a Clear button that
  issues DELETE to remove the rating.
- Reads the server-rendered value from a new data-rating attribute on
  <body> during the bookDetail component init().

The community rating block is left as a display-only renderStars render
since it is imported metadata, not a user rating.

templates/book_detail_templ.go is regenerated (also picking up templ
v0.3.1020 reformatting of the generated output).
2026-07-30 13:08:00 -04:00
john-okeefe 05370d236a feat(ui): display media counts in library switcher
The UI had no surface showing how many media items have been imported.
Surface the total in the library switcher shown on the Dashboard, Series,
and Collections pages (via the LibrarySwitcher component) and in the
Bookshelf's inline library filter.

- Add a MediaCount field to LibraryData and a TotalMediaCount helper to
  sum counts for the "All Libraries" / "All Books" option.
- resolveLibrary() now fetches per-library counts (one query) and maps
  them onto each LibraryData entry, so the switcher reflects the active
  scope without changing the component's signature.
- Each library option renders "(N)" and the "All" option renders the
  grand total across the user's visible libraries.

The "All" total is the sum of the user's visible libraries, correctly
respecting per-user library visibility rather than a raw global count.

Regenerated templ files for library_switcher and bookshelf.
2026-07-30 11:41:13 -04:00
john-okeefe ce3ab8bcbc Fix invalid templ conditional syntax in metadata editor modal
The Manga Type and Reading Direction select options used invalid inline
if syntax: 'if cond { selected }' which is not valid templ. Replaced
with the correct templ attribute syntax: selected={ boolExpression }
which properly renders the selected attribute when true and omits it
when false.

Also add for/id attributes to link labels to their select elements
for accessibility (manga_type and reading_direction).

Affected lines: book_detail_modals.templ:478-496
2026-06-08 21:19:06 -04:00
john-okeefe 423a1c0bf7 feat(sync): use page index as canonical locator for fixed-layout & comic formats
Fixed-layout EPUBs, PDFs, DjVu, and comic archives (cbz/cbr/cb7/cbt)
are page-based: each page is a fixed image, so a page index is an exact,
universal locator regardless of screen size or device. Sync previously
treated these like reflowable content (CFI-first restore, percentage
fallback, character-offset math), which was both wrong and lossy. This
makes the page index the canonical position for fixed-layout and comic
formats while leaving the reflowable path byte-for-byte unchanged.

Backend:
- progress.go SaveProgress: branch on mediaItem.FormatGroup. For
  fixed_layout/comic_archive derive percentage from current_page/
  total_pages and skip the CFI/character-offset back-fills (meaningless
  for image content). The reflowable derivation block is preserved
  verbatim under an else.
- kobo.go: Kobo only sends a percentage, so for fixed-layout/comic
  formats derive CurrentPage via PercentageToPage(percentage, pageCount)
  using the media item's known page count, so Kobo->web lands on the
  exact page.

Reader:
- reader.templ readerInitExpr: pass savedPage/savedTotalPages to the
  reader config for fixed_layout/comic_archive formats.
- reader.ts: when isFixedLayout and savedPage is present, restore via
  view.init({ lastLocation: savedPage - 1 }) (a bare number navigates
  foliate directly to the section index). Reflowable falls through to
  the existing CFI->percentage path, unchanged.
2026-06-07 00:03:46 -04:00
john-okeefe 3170aa6b77 feat(setup): collect server base URL during first-run wizard
Add a Server URL field to the admin-account step of the setup wizard so
the public base URL (used for device sync, OPDS feed, and API endpoints)
is configured up front instead of requiring a later visit to admin
settings.

- setup.templ: base URL input on the admin step plus a summary entry
- setup.ts: default baseUrl to window.location.origin and persist it via
  PUT /system/config ({ base_url }) right after the admin account is
  created, with a non-fatal warning if the save fails
2026-06-07 00:03:01 -04:00
john-okeefe f75d68bf66 feat(ui): add first-run setup wizard with 4 guided steps
Add a single-page multi-step setup wizard that guides new users
through initial configuration:

Step 1 - Admin Registration: Creates the first user (auto-admin)
  using the existing POST /api/auth/register endpoint, with
  real-time password validation and confirmation matching.

Step 2 - Library Creation: Create one or more libraries (Ebooks,
  Audiobooks, Comics, Manga) using POST /api/libraries. Libraries
  list updates inline as they're added.

Step 3 - Folder Configuration: Add filesystem folders to each
  library using the existing GET /api/libraries/browse endpoint for
  a visual directory browser. Folders are attached via POST
  /api/libraries/:id/folders.

Step 4 - Initial Scan: Triggers a manual scan of all libraries via
  POST /api/libraries/scan with real-time progress polling using the
  existing scan status endpoint.

On completion, the wizard calls PUT /api/setup/complete and sets the
selectedLibrary cookie to the first library's UUID, ensuring the
dashboard loads with populated content instead of an empty 'All
Libraries' view. Handles the edge case where a stale JWT from a
previous database instance triggers an 'already exists' error by
auto-advancing to step 2.

The wizard reuses all existing API calls, Alpine.js utilities, and
form validation functions — no backend logic was duplicated.
2026-06-06 00:04:09 -04:00
john-okeefe 0f225c39b9 fix(templ): correct @click attribute interpolation syntax
Templ requires Go expression interpolation for dynamic attributes,
not string embedding. Change from @click="func('{ id }')" to
@click={ "func('" + id + "')" } for device ID and registration ID
buttons.
2026-06-02 19:46:29 -04:00
john-okeefe bc59159cde feat(ui): inline conflict resolution with Keep This button
Replace the 'Go to Conflicts Page' link with inline conflict resolution.
Each conflict source now has a 'Keep This' button that resolves the
conflict directly from the book detail page.

- Conflict data now keyed by source name (koreader, web) instead of
  new/existing, with Source and Timestamp fields
- Display percentage scaled correctly (* 100)
- Fix page field name from current_page to page
- Add conflict resolution JavaScript in book-detail.ts
- Add 10-minute cooldown after resolution to prevent re-detection
2026-06-02 19:46:17 -04:00
john-okeefe f57d64563b fix(reader): scale percentage correctly, add 5s init guard
- reader.templ: divide DB percentage (0-1) by 100 was wrong; actually
  the router multiplies by 100, so template now divides by 100 to get
  back to 0-1 range for foliate-js savedPercentage
- reader.ts: add 5-second initTime guard to prevent overwriting
  existing progress with initial position on page load
2026-06-02 19:46:03 -04:00
john-okeefe d78a182f0d feat(header): add responsive mobile hamburger menu with slide-down panel
On viewports below 970px the header now collapses to a compact bar with
only the Bookhoard logo and a hamburger button. Clicking the hamburger
reveals a slide-down panel containing:

- Full-text search input (wired to the existing debounced search API)
- Navigation links (Library, All Books, Series, Collections, Progress, Devices)
- Collapsible theme switcher with all 7 themes and 4 bookshelf backgrounds
- User section: profile/admin/logout when logged in, inline login form when logged out

Changes:
- templates/header.templ: add hamburger button, mobile panel with all controls,
  hide desktop search/theme/user controls below nav breakpoint
- web/src/header.ts: add mobileMenuOpen state to Alpine header component
- web/src/search.ts: refactor initializeSearch to wire both desktop and mobile
  search inputs, track active input for results container placement
- tailwind.config.ts: add custom 'nav' screen breakpoint at 970px so the
  mobile menu activates before the search bar becomes unusable
- web/static/style.css: rebuilt with new nav breakpoint utility classes
2026-05-24 21:20:36 -04:00
john-okeefe 13260d8c1f chore(templates): regenerate all _templ.go files with templ v0.3.1020
Upgrade from templ v0.3.1001 to v0.3.1020. Generated code changes include
JoinStringErrs -> ResolveAttributeValue and removal of manual EscapeString
calls (now handled internally by ResolveAttributeValue).
2026-05-24 21:20:24 -04:00
john-okeefe 0a9cde0fc8 chore(templates): regenerate all _templ.go files
Regenerate all templ-generated Go files. These changes are caused
by running templ generate with a slightly different CLI version
(v0.3.1001) than the go.mod dependency (v0.3.1020), resulting in
minor formatting/import diffs across all templates. No functional
changes.
2026-05-18 17:53:12 -04:00