Rebuild both UIs on the Cinder design (#10)
Implements the **Cinder** design (Claude Design doc `cfa39183`) across both UI surfaces, self-hosts the fonts it depends on, and writes down the two documents that keep the result maintainable. ## What changed **Web UI** — rebuilt on the design's visual language: editorial serif, containerless sheets divided by ash hairlines, one 760px measure, no radii and no shadows. The organising rule is that *heat is typographic*: only a series with an unread chapter is crimson (title on an ember underline, cover foot rule, `Ch N out`, play icon), and favourites get brass rather than borrowing the accent. Both states hang off two classes on the `<article>` (`is-new`, `is-dim`), so sub-elements inherit the state instead of re-deriving it. The six-cell action strip is `flex-basis: 100%` inside the row, which is what lets one piece of markup be a full-width strip with 46px thumb targets on a phone and a group of 40px squares beside the row on a desktop — no duplicate template branches. Icons moved to a sprite (`templates/icons.html`); htmx-swapped cards reference the page's symbols, so a row no longer carries a screenful of inline SVG. **Userscript panel** — repainted in the same tokens. The panel and the web UI are the same product on the same phone, and the old purple-on-charcoal panel read as a different application once the web UI moved. Structure, ids and classes are untouched, and the edge tab keeps its geometry, `touch-action` and `#hit` sizing. **Fonts are self-hosted** — five latin-subset woff2 files (~120 KB) embedded via the existing `//go:embed static`. Loading them from Google would lose the design's character exactly where it is used most: Bromite users routinely block Google's font domains, and the backend is reachable over a LAN with no internet route. `staticHandler` registers the `.woff2` MIME type, which Go's table lacks and the scratch image has no `/etc/mime.types` for. **Docs** — `docs/design-system.md` records the rules a stylesheet cannot state (what the ember is reserved for, why light mode is a re-tuning rather than an inversion, which details are load-bearing) so a future agent does not re-derive them from the CSS. `REDEPLOY.md` covers the operation actually performed every time, which `DEPLOY.md` reduced to two lines. ## Commits Each is one logical change and builds on its own: | | | |---|---| | `4d69e54` | `listView.NewCount` — data for the Updated badge, no markup | | `e940b96` | Web UI rebuilt on Cinder (CSS, templates, sprite, `filter.js`) | | `686fcc1` | Userscript panel repainted in the same tokens | | `ce7e93d` | Self-hosted webfonts + `.woff2` MIME registration | | `a547cc9` | `docs/design-system.md` | | `b20ecf1` | `REDEPLOY.md` | ## Verification - `go test ./...` — 187 pass. Userscript logic tests — 14 pass. - Screenshotted at 390px and 1180px, in dark and light, across All / Archived / Finished / empty / chapter-form / confirm-row. - Fonts: a run with `fonts.googleapis.com` and `fonts.gstatic.com` blocked still reports all five faces `loaded`; served as `200 font/woff2`. - The three `REDEPLOY.md` backup/restore commands were run, not assumed. `:ro` on the source volume fails (`unable to open database file` — WAL needs to create `-shm`), and a restored file lands root-owned while the container runs as uid 65532, so reads succeed and writes fail. Both are documented with the reason. ## Note for the reviewer The panel restyle is token-level only: the design doc covers the web UI, so the panel's *structure* has no reference to follow and was deliberately left alone. Deploying this needs a rebuild — templates, CSS and fonts are `//go:embed`ed, so pulling alone changes nothing. Reviewed-on: #10 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #10.
This commit is contained in:
+24
-12
@@ -26,29 +26,41 @@ function setActiveTab(el) {
|
||||
}
|
||||
|
||||
// The chapter-edit form and the delete confirm row are the two per-card
|
||||
// disclosure panels; only one makes sense open at a time.
|
||||
// disclosure panels; only one makes sense open at a time. The button that
|
||||
// owns an open panel carries .open, which is how the strip shows which cell
|
||||
// the panel belongs to.
|
||||
function closeCardPanels(key) {
|
||||
var form = document.getElementById("chapter-form-" + key);
|
||||
var confirm = document.getElementById("confirm-row-" + key);
|
||||
if (form) form.hidden = true;
|
||||
if (confirm) confirm.hidden = true;
|
||||
var card = document.getElementById("card-" + key);
|
||||
if (card) {
|
||||
card.querySelectorAll(".actions .open").forEach(function (b) {
|
||||
b.classList.remove("open");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function togglePanel(key, panelId, buttonSelector) {
|
||||
var panel = document.getElementById(panelId + key);
|
||||
if (!panel) return null;
|
||||
var opening = panel.hidden;
|
||||
closeCardPanels(key);
|
||||
panel.hidden = !opening;
|
||||
var card = document.getElementById("card-" + key);
|
||||
var button = card && card.querySelector(buttonSelector);
|
||||
if (button) button.classList.toggle("open", opening);
|
||||
return panel;
|
||||
}
|
||||
|
||||
function toggleChapterForm(key) {
|
||||
var form = document.getElementById("chapter-form-" + key);
|
||||
if (!form) return;
|
||||
var opening = form.hidden;
|
||||
closeCardPanels(key);
|
||||
form.hidden = !opening;
|
||||
if (!form.hidden) form.querySelector("input").focus();
|
||||
var form = togglePanel(key, "chapter-form-", ".actions .pencil");
|
||||
if (form && !form.hidden) form.querySelector("input").focus();
|
||||
}
|
||||
|
||||
function toggleConfirmRow(key) {
|
||||
var row = document.getElementById("confirm-row-" + key);
|
||||
if (!row) return;
|
||||
var opening = row.hidden;
|
||||
closeCardPanels(key);
|
||||
row.hidden = !opening;
|
||||
togglePanel(key, "confirm-row-", ".actions .remove");
|
||||
}
|
||||
|
||||
// A failed favourite/chapter/delete request leaves the card in place (htmx
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+618
-367
File diff suppressed because it is too large
Load Diff
@@ -7,19 +7,29 @@
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>mangaBookmark</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/static/fonts/ibm-plex-mono-500-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
<script src="/static/filter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "icons" .}}
|
||||
<div class="sheet">
|
||||
<header class="topbar">
|
||||
<h1>mangaBookmark</h1>
|
||||
<h1 class="brand">manga<em>Bookmark</em></h1>
|
||||
<form method="post" action="/logout">
|
||||
<button type="submit" class="ghost">Log out</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<input id="search" class="search" type="search" placeholder="Search titles…"
|
||||
autocomplete="off" aria-label="Search titles">
|
||||
{{/* Search sits above the tabs on a phone and folds into the tab row on a
|
||||
wider screen — one flex container, order swapped in CSS. */}}
|
||||
<div class="chrome">
|
||||
<div class="searchbar">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-search"/></svg>
|
||||
<input id="search" class="search" type="search" placeholder="Find a title"
|
||||
autocomplete="off" aria-label="Search titles">
|
||||
</div>
|
||||
|
||||
<nav class="tabs" role="tablist">
|
||||
<a role="tab" href="/?tab=all" class="{{if eq .Tab "all"}}active{{end}}"
|
||||
@@ -27,7 +37,8 @@
|
||||
hx-push-url="/?tab=all" hx-on::after-request="setActiveTab(this)">All</a>
|
||||
<a role="tab" href="/?tab=new" class="tab-new {{if eq .Tab "new"}}active{{end}}"
|
||||
hx-get="/ui/list?tab=new" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=new" hx-on::after-request="setActiveTab(this)">Updated</a>
|
||||
hx-push-url="/?tab=new" hx-on::after-request="setActiveTab(this)">Updated{{if .NewCount}}
|
||||
<span class="count">{{.NewCount}}</span>{{end}}</a>
|
||||
<a role="tab" href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
|
||||
hx-get="/ui/list?tab=fav" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=fav" hx-on::after-request="setActiveTab(this)">Favourites</a>
|
||||
@@ -38,20 +49,23 @@
|
||||
hx-get="/ui/list?tab=finished" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=finished" hx-on::after-request="setActiveTab(this)">Finished</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{if .Recent}}
|
||||
<section class="recent">
|
||||
<h2>Continue reading</h2>
|
||||
<div class="recent-strip">
|
||||
{{range .Recent}}
|
||||
<a class="recent-card" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">
|
||||
<a class="recent-card {{if .HasNewChapter}}is-new{{end}}" href="{{.ContinueURL}}"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
<span class="recent-cover">
|
||||
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
|
||||
{{else}}<span class="cover-blank" aria-hidden="true">{{.Initial}}</span>{{end}}
|
||||
{{if .HasNewChapter}}<span class="new-ribbon new-ribbon-sm">New</span>{{end}}
|
||||
{{else}}<span class="monogram" aria-hidden="true">{{.Initial}}</span>{{end}}
|
||||
{{if .HasNewChapter}}<span class="foot-rule"></span>
|
||||
{{else if .Favorite}}<span class="foot-rule brass"></span>{{end}}
|
||||
</span>
|
||||
<span class="recent-title">{{.Title}}</span>
|
||||
<span class="recent-chapter">Ch {{.LastChapter}}</span>
|
||||
<span class="recent-chapter">Ch {{.LastChapter}}{{if .HasNewChapter}} · New{{end}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -61,6 +75,7 @@
|
||||
<main id="list" class="list">
|
||||
{{template "list" .}}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
+67
-64
@@ -1,111 +1,114 @@
|
||||
{{define "card"}}
|
||||
<article class="card" id="card-{{.Key}}" data-title="{{.Title}}">
|
||||
<a class="cover" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">
|
||||
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
|
||||
{{else}}<span class="cover-blank" aria-hidden="true">{{.Initial}}</span>{{end}}
|
||||
{{if .HasNewChapter}}<span class="new-ribbon">New</span>{{end}}
|
||||
</a>
|
||||
<div class="body">
|
||||
<h3 class="title">{{.Title}}</h3>
|
||||
<p class="meta">
|
||||
<span class="site site-{{.Site}}">{{.Site}}</span>
|
||||
<span class="chapter">Ch {{.LastChapter}}</span>
|
||||
{{if .HasNewChapter}}<span class="new-chapter">Ch {{.LatestChapter}} out</span>{{end}}
|
||||
</p>
|
||||
{{/* One sheet per series. is-new turns the title crimson over an ember rule;
|
||||
is-dim sinks archived and finished rows into italic grey. */}}
|
||||
<article class="card{{if eq .Status "reading"}}{{if .HasNewChapter}} is-new{{end}}{{else}} is-dim{{end}}"
|
||||
id="card-{{.Key}}" data-title="{{.Title}}">
|
||||
<div class="row">
|
||||
<a class="cover" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
|
||||
tabindex="-1" aria-hidden="true">
|
||||
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
|
||||
{{else}}<span class="monogram">{{.Initial}}</span>{{end}}
|
||||
{{if and (eq .Status "reading") .HasNewChapter}}<span class="foot-rule"></span>
|
||||
{{else if .Favorite}}<span class="foot-rule brass"></span>{{end}}
|
||||
</a>
|
||||
<div class="body">
|
||||
<div class="title-line">
|
||||
<h3 class="title">{{.Title}}</h3>
|
||||
{{if .Favorite}}
|
||||
<svg class="fav-mark" viewBox="0 0 24 24" aria-label="Favourite" role="img"><use href="#i-star-on"/></svg>
|
||||
{{end}}
|
||||
</div>
|
||||
<p class="meta">
|
||||
<span class="site-{{.Site}}">{{.Site}}</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="chapter">Ch {{.LastChapter}}</span>
|
||||
{{if and (eq .Status "reading") .HasNewChapter}}
|
||||
<span class="sep">/</span>
|
||||
<span class="new-chapter">Ch {{.LatestChapter}} out</span>
|
||||
{{end}}
|
||||
{{if eq .Status "archived"}}
|
||||
<span class="sep">/</span>
|
||||
<span class="state">archived</span>
|
||||
{{else if eq .Status "finished"}}
|
||||
<span class="sep">/</span>
|
||||
<span class="state"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-check"/></svg>finished</span>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="primary" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
|
||||
<a class="play" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
|
||||
title="Continue reading" aria-label="Continue reading">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
|
||||
<path d="M8 4.8a1 1 0 0 1 1.52-.85l10 7.2a1 1 0 0 1 0 1.7l-10 7.2A1 1 0 0 1 8 19.2z"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-play"/></svg>
|
||||
</a>
|
||||
<button class="icon {{if .Favorite}}on{{end}}"
|
||||
title="Favourite" aria-label="Toggle favourite"
|
||||
<button class="{{if .Favorite}}on{{end}}"
|
||||
title="{{if .Favorite}}Remove from favourites{{else}}Add to favourites{{end}}"
|
||||
aria-label="Toggle favourite"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/favorite"
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="{{if .Favorite}}currentColor{{else}}none{{end}}"
|
||||
stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"
|
||||
aria-hidden="true" focusable="false">
|
||||
<path d="M12 3.7l2.63 5.33 5.87.86-4.25 4.14 1 5.85L12 17.31l-5.25 2.57 1-5.85L3.5 9.89l5.87-.86z"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-star{{if .Favorite}}-on{{end}}"/></svg>
|
||||
</button>
|
||||
<button class="icon" title="Set chapter" aria-label="Set chapter"
|
||||
<button class="pencil" title="Set chapter" aria-label="Set chapter"
|
||||
onclick="toggleChapterForm('{{.Key}}')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4 20h4L19.3 8.7a1.5 1.5 0 0 0 0-2.12l-1.88-1.88a1.5 1.5 0 0 0-2.12 0L4 16z"/>
|
||||
<path d="M13.8 7.2l3 3"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-pencil"/></svg>
|
||||
</button>
|
||||
{{if eq .Status "finished"}}
|
||||
<button class="icon on" title="Restore to reading" aria-label="Restore to reading"
|
||||
<button class="restore" title="Restore to reading" aria-label="Restore to reading"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"reading"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4 12a8 8 0 1 1 2.6 5.9M4 12V7M4 12h5"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-undo"/></svg>
|
||||
</button>
|
||||
{{else}}
|
||||
{{if eq .Status "archived"}}
|
||||
<button class="icon on" title="Restore to reading" aria-label="Restore to reading"
|
||||
<button class="restore" title="Restore to reading" aria-label="Restore to reading"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"reading"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4 12a8 8 0 1 1 2.6 5.9M4 12V7M4 12h5"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-undo"/></svg>
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="icon" title="Archive" aria-label="Archive"
|
||||
<button title="Archive" aria-label="Archive"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"archived"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M3.5 6.4h17v3.2h-17zM5 9.6h14V19H5zM10 13h4"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-box"/></svg>
|
||||
</button>
|
||||
{{end}}
|
||||
<button class="icon" title="Mark finished" aria-label="Mark finished"
|
||||
<button title="Mark finished" aria-label="Mark finished"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"finished"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4.5 12.5l5 5 10-11"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-check"/></svg>
|
||||
</button>
|
||||
{{end}}
|
||||
<button class="icon danger" title="Remove" aria-label="Remove"
|
||||
<button class="remove" title="Remove" aria-label="Remove"
|
||||
onclick="toggleConfirmRow('{{.Key}}')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4.5 6.8h15M9.8 6.8V4.6h4.4v2.2M7.3 6.8l.9 12.6h7.6l.9-12.6"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-trash"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<form class="chapter-form" id="chapter-form-{{.Key}}" hidden
|
||||
hx-post="/ui/bookmarks/{{.Key}}/chapter"
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
|
||||
{{if .LatestChapter}}<p class="hint">Latest known: Ch {{.LatestChapter}}</p>{{end}}
|
||||
</div>
|
||||
<form class="chapter-form" id="chapter-form-{{.Key}}" hidden
|
||||
hx-post="/ui/bookmarks/{{.Key}}/chapter"
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
|
||||
{{if .LatestChapter}}<p class="hint">Latest known: Ch {{.LatestChapter}}</p>{{end}}
|
||||
<div class="field">
|
||||
<input name="chapter" type="number" step="0.1" min="0"
|
||||
value="{{.LastChapterNum}}" aria-label="Chapter number" required>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
<div class="confirm-row" id="confirm-row-{{.Key}}" hidden>
|
||||
<span>Remove this?</span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="confirm-row" id="confirm-row-{{.Key}}" hidden>
|
||||
<span>Remove this?</span>
|
||||
<div>
|
||||
<button class="danger-solid"
|
||||
hx-delete="/ui/bookmarks/{{.Key}}"
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">Remove</button>
|
||||
<button type="button" onclick="toggleConfirmRow('{{.Key}}')">Cancel</button>
|
||||
</div>
|
||||
<p class="error-inline" hidden></p>
|
||||
</div>
|
||||
<p class="error-inline" hidden></p>
|
||||
</article>
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{{define "icons"}}
|
||||
<!-- One sprite per page. Card fragments swapped in by htmx reference these
|
||||
symbols by id, so the markup for a row stays a handful of <use> tags. -->
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
||||
<symbol id="i-play" viewBox="0 0 24 24"><path d="M8 5l11 7-11 7z" fill="currentColor"/></symbol>
|
||||
<symbol id="i-star" viewBox="0 0 24 24"><path d="M12 3.6l2.6 5.6 6 .8-4.4 4.2 1.1 6-5.3-2.9-5.3 2.9 1.1-6-4.4-4.2 6-.8z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-star-on" viewBox="0 0 24 24"><path d="M12 3.6l2.6 5.6 6 .8-4.4 4.2 1.1 6-5.3-2.9-5.3 2.9 1.1-6-4.4-4.2 6-.8z" fill="currentColor" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24"><path d="M4 20h4L19 9l-4-4L4 16z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-box" viewBox="0 0 24 24"><path d="M3 7h18v4H3zM5 11v9h14v-9M10 15h4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24"><path d="M4 12.5l5.2 5.5L20 6.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16M9.5 7V4h5v3M6.5 7l1 13h9l1-13M10.5 11v6M13.5 11v6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
||||
<symbol id="i-undo" viewBox="0 0 24 24"><path d="M4 9h9.5a5 5 0 010 10H8M4 9l4.2-4.2M4 9l4.2 4.2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M15.3 15.3L20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
|
||||
</svg>
|
||||
{{end}}
|
||||
@@ -2,16 +2,14 @@
|
||||
{{if .Items}}
|
||||
{{range .Items}}{{template "card" .}}{{end}}
|
||||
{{else if eq .Tab "fav"}}
|
||||
<p class="empty">No favourites yet. Star a series to pin it here.</p>
|
||||
<div class="empty"><strong>No favourites yet.</strong><p>Star a series to pin it here.</p></div>
|
||||
{{else if eq .Tab "new"}}
|
||||
<p class="empty">Nothing new. Every series is caught up to its latest chapter.</p>
|
||||
<div class="empty hot"><strong>Nothing new.</strong><p>Every series is caught up to its latest chapter.</p></div>
|
||||
{{else if eq .Tab "archived"}}
|
||||
<p class="empty">Nothing archived. Shelve a series to park it here — it keeps getting checked for new chapters.</p>
|
||||
<div class="empty"><strong>Nothing archived.</strong><p>Shelve a series to park it here — it keeps getting checked for new chapters.</p></div>
|
||||
{{else if eq .Tab "finished"}}
|
||||
<p class="empty">Nothing finished yet.</p>
|
||||
<div class="empty"><strong>Nothing finished yet.</strong><p>Mark a series finished and it moves out of your reading list.</p></div>
|
||||
{{else}}
|
||||
<p class="empty">
|
||||
Nothing here yet. Bookmarks appear once the userscript records a chapter.
|
||||
</p>
|
||||
<div class="empty"><strong>Nothing here yet.</strong><p>Bookmarks appear once the userscript records a chapter.</p></div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -7,15 +7,21 @@
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>mangaBookmark</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<body>
|
||||
<main class="login-card">
|
||||
<h1>mangaBookmark</h1>
|
||||
<div>
|
||||
<span class="eyebrow">Private library</span>
|
||||
<h1>manga<em>Bookmark</em></h1>
|
||||
</div>
|
||||
<form method="post" action="/login">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password"
|
||||
autocomplete="current-password" autofocus required>
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<div>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password"
|
||||
autocomplete="current-password" autofocus required>
|
||||
</div>
|
||||
<p class="error">{{.Error}}</p>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
+16
-2
@@ -7,6 +7,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -38,6 +39,11 @@ type listView struct {
|
||||
Tab string // "all", "fav", or "new"
|
||||
Recent []Bookmark
|
||||
Items []Bookmark
|
||||
// NewCount is the badge on the Updated tab: how many series being read
|
||||
// have a chapter out that has not been read. It is counted over the whole
|
||||
// reading set, not the active tab, so the badge does not change meaning as
|
||||
// the user moves between tabs.
|
||||
NewCount int
|
||||
}
|
||||
|
||||
// Initial is the monogram the templates show in place of a cover when the
|
||||
@@ -93,6 +99,12 @@ func staticHandler() http.Handler {
|
||||
if err != nil {
|
||||
panic("embed static: " + err.Error())
|
||||
}
|
||||
// Go's built-in table has no .woff2 and the scratch image has no
|
||||
// /etc/mime.types, so without this the fonts go out as
|
||||
// application/octet-stream.
|
||||
if err := mime.AddExtensionType(".woff2", "font/woff2"); err != nil {
|
||||
panic("woff2 mime: " + err.Error())
|
||||
}
|
||||
files := http.FileServer(http.FS(sub))
|
||||
return http.StripPrefix("/static/", http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -178,12 +190,14 @@ func (h *webHandler) buildListView(tab string) (listView, error) {
|
||||
recent = recent[:recentCount]
|
||||
}
|
||||
|
||||
withNew := filterBookmarks(reading, func(b Bookmark) bool { return b.HasNewChapter() })
|
||||
|
||||
var items []Bookmark
|
||||
switch tab {
|
||||
case "fav":
|
||||
items = filterBookmarks(reading, func(b Bookmark) bool { return b.Favorite })
|
||||
case "new":
|
||||
items = filterBookmarks(reading, func(b Bookmark) bool { return b.HasNewChapter() })
|
||||
items = withNew
|
||||
case "archived":
|
||||
items = filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusArchived })
|
||||
case "finished":
|
||||
@@ -192,7 +206,7 @@ func (h *webHandler) buildListView(tab string) (listView, error) {
|
||||
tab = "all"
|
||||
items = reading
|
||||
}
|
||||
return listView{Tab: tab, Recent: recent, Items: items}, nil
|
||||
return listView{Tab: tab, Recent: recent, Items: items, NewCount: len(withNew)}, nil
|
||||
}
|
||||
|
||||
func (h *webHandler) uiList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user