diff --git a/src/web/app.ts b/src/web/app.ts index 77c0555..f3aad26 100644 --- a/src/web/app.ts +++ b/src/web/app.ts @@ -2,11 +2,15 @@ import { LITTERBOX_TIMES, type HostId, type HostOptions, type LitterboxTime } fr import { HOSTS } from "../core/hosting/registry"; import { coverFormUrl } from "../core/mangabaka/client"; import { CONTENT_RATINGS, GRANULARITIES, type ContentRating, type Granularity } from "../core/model"; +import type { RunResult } from "../core/pipeline"; +import { carryEdits, fromSaved, saveAndPrune, toSaved, type HistoryStore, type Marks } from "../core/saved"; import { loadToken, saveToken, type KeyValueStore, type PreparedSubmission, type Session } from "../core/session"; +import { SOURCES } from "../core/sources/registry"; import { zipStore } from "../core/zip"; +import { renderRecent } from "./recent"; import { renderCards, renderPresent, renderPreview } from "./render"; import { saveFile } from "./save-file"; -import { GRANULARITY_LABELS, defaultHost, hostChoices, stageText, type ServerConfig } from "./view-model"; +import { GRANULARITY_LABELS, defaultHost, hostChoices, relativeTime, stageText, type ServerConfig } from "./view-model"; export const USERHASH_KEY = "mbseed.catbox-userhash"; export const VIEW_KEY = "mbseed.view"; @@ -20,8 +24,8 @@ export interface AppOptions { catboxNeedsServer: boolean; /** Userscript: the MangaBaka series page it runs on. */ seriesLink?: string; - /** Called after "Download selected (ZIP)" handed the file to the browser. */ - onDownloaded?: () => void; + /** The page's history of searches (spec 002 §3); the userscript has none. */ + history?: HistoryStore | null; } /** Wires APP_HTML to a Session. Nothing is uploaded or sent without a click (spec §1). */ @@ -72,6 +76,15 @@ export function mountApp(o: AppOptions): void { previewDiff: q("preview-diff"), confirm: q("confirm"), submitStatus: q("submit-status"), + recent: q("recent"), + recentList: q("recent-list"), + recentStatus: q("recent-status"), + recentClear: q("recent-clear"), + recentShelfWrap: q("recent-shelf-wrap"), + recentShelf: q("recent-shelf"), + foundLine: q("found-line"), + foundWhen: q("found-when"), + searchAgain: q("search-again"), }; const plates = [...o.root.querySelectorAll(".mbseed-plates [data-stage]")]; let currentStage = ""; @@ -92,6 +105,11 @@ export function mountApp(o: AppOptions): void { let running: AbortController | null = null; let stopHistory: AbortController | null = null; let prepared: PreparedSubmission | null = null; + /** The history entry on screen: what was done with it, and when its search ran. */ + let marks: Marks = {}; + let foundAt = ""; + let currentId: number | null = null; + let saveTimer: ReturnType | undefined; const fill = (select: HTMLSelectElement, values: readonly (string | number)[], label: (v: string) => string, selected: string) => select.replaceChildren(...values.map(String).map((v) => new Option(label(v), v, false, v === selected))); @@ -163,6 +181,109 @@ export function mountApp(o: AppOptions): void { ui.confirm.hidden = true; ui.previewDiff.replaceChildren(); draw(); + scheduleSave(); + } + + async function refreshRecent(): Promise { + if (!o.history) return; + const items = await o.history.list().catch(() => []); + ui.recent.hidden = items.length === 0; + ui.recentShelfWrap.hidden = items.length === 0; + renderRecent(ui.recentList, ui.recentShelf, items, currentId, new Date(), { open: (id) => void open(id), remove: (id) => void forget(id) }); + } + + /** Spec 002 §3.1: the entry is rewritten after the search, each edit, rehosting, a download and filing. */ + function save(): void { + clearTimeout(saveTimer); + const r = o.session.result; + const input = o.session.input; + if (!o.history || !r || !input) return; + const entry = toSaved({ + result: r, cards: o.session.cards, makeDefault: o.session.makeDefault, input, + rating: ui.ratingAll.value as ContentRating, marks, foundAt, now: new Date(), + }); + saveAndPrune(o.history, entry) + .then(() => status(ui.recentStatus, "")) + .catch(() => status(ui.recentStatus, "This search couldn't be saved: the browser refused the storage.", true)) + .finally(() => void refreshRecent()); + } + + function scheduleSave(): void { + clearTimeout(saveTimer); + if (o.history) saveTimer = setTimeout(save, 500); + } + + function mark(patch: Marks): void { + marks = { ...marks, ...patch }; + save(); + } + + /** Shows a finished search: plates, notes, the series head and the shelf. */ + function show(r: RunResult, historyOff: boolean): void { + markStage("compare", "done"); + setCount("series", `#${r.seriesId}`); + setCount("source", r.series.language); + if (historyOff) setCount("history", "off"); + setCount("inspect", String(r.fresh.length + r.present.length)); + setCount("compare", `${r.present.length} on MangaBaka`); + const notes = [...r.warnings]; + const landing = r.fresh.filter((c) => c.shape === "tall").length; + if (landing) { + notes.push(`${landing} tall image${landing === 1 ? " looks" : "s look"} like landing-page art and start${landing === 1 ? "s" : ""} unselected.`); + } + ui.warnings.replaceChildren(...notes.map((textContent) => Object.assign(document.createElement("li"), { textContent }))); + ui.progress.max = 1; + ui.progress.value = 1; + ui.seriesTitle.textContent = r.mbSeries.title; + ui.seriesSource.textContent = `← ${r.source.name} “${r.series.title}” · ${r.series.language}`; + ui.matureWarning.hidden = !r.series.mature; + ui.formLink.href = coverFormUrl(r.seriesId); + ui.review.hidden = false; + draw(); + } + + /** Clears what belonged to the previous series on screen. */ + function resetReview(): void { + prepared = null; + ui.confirm.hidden = true; + ui.previewDiff.replaceChildren(); + ui.linkConfirm.checked = false; + for (const line of [ui.rehostStatus, ui.manualStatus, ui.submitStatus]) status(line, ""); + } + + async function open(seriesId: number): Promise { + if (!o.history || running) return; + save(); + try { + const entry = await o.history.get(seriesId); + if (!entry) throw new Error("That entry is no longer in the history."); + const back = fromSaved(entry, SOURCES, Date.now()); + o.session.restore(back.result, back.cards, back.makeDefault, back.input); + resetReview(); + if (!ui.mbLink.readOnly) ui.mbLink.value = back.input.mangabakaLink; + ui.sourceLink.value = back.input.sourceLink; + ui.historyOn.checked = back.input.history !== null; + if (back.input.history !== null) ui.historyEvery.value = String(back.input.history); + ui.ratingAll.value = back.rating; + marks = back.marks; + foundAt = back.foundAt; + currentId = seriesId; + for (const plate of plates) setCount(plate.dataset.stage ?? "", ""); + show(back.result, back.input.history === null); + ui.foundWhen.textContent = `Found ${relativeTime(back.foundAt)}.`; + ui.foundLine.hidden = false; + status(ui.stage, `Opened from the history: ${back.result.fresh.length} new cover(s); ${back.result.present.length} already on MangaBaka.`); + } catch (err) { + status(ui.stage, errorText(err), true); + } + void refreshRecent(); + } + + async function forget(seriesId: number): Promise { + if (!o.history) return; + await o.history.remove(seriesId).catch(() => undefined); + if (currentId === seriesId) currentId = null; + void refreshRecent(); } function draw(): void { @@ -195,9 +316,8 @@ export function mountApp(o: AppOptions): void { ui.cancel.hidden = false; ui.warnings.replaceChildren(); ui.review.hidden = true; - ui.confirm.hidden = true; - ui.previewDiff.replaceChildren(); - ui.linkConfirm.checked = false; + resetReview(); + ui.foundLine.hidden = true; for (const plate of plates) setCount(plate.dataset.stage ?? "", ""); status(ui.stage, "Starting…"); try { @@ -226,26 +346,15 @@ export function mountApp(o: AppOptions): void { }, ui.ratingAll.value as ContentRating, ); - markStage("compare", "done"); - setCount("series", `#${r.seriesId}`); - setCount("source", r.series.language); - if (ui.historyOn.checked === false) setCount("history", "off"); - setCount("compare", `${r.present.length} on MangaBaka`); - const landing = r.fresh.filter((c) => c.shape === "tall").length; - if (landing) { - ui.warnings.append(Object.assign(document.createElement("li"), { - textContent: `${landing} tall image${landing === 1 ? " looks" : "s look"} like landing-page art and start${landing === 1 ? "s" : ""} unselected.`, - })); - } + // Searching a series again keeps what the user did with it (spec 002 §3.3). + const previous = o.history ? await o.history.get(r.seriesId).catch(() => null) : null; + if (previous) carryEdits(previous.cards, o.session.cards, Date.now()); + marks = previous?.marks ?? {}; + foundAt = new Date().toISOString(); + currentId = r.seriesId; + show(r, !ui.historyOn.checked); status(ui.stage, `Found ${r.fresh.length} new cover(s); ${r.present.length} already on MangaBaka.`); - ui.progress.max = 1; - ui.progress.value = 1; - ui.seriesTitle.textContent = r.mbSeries.title; - ui.seriesSource.textContent = `← ${r.source.name} “${r.series.title}” · ${r.series.language}`; - ui.matureWarning.hidden = !r.series.mature; - ui.formLink.href = coverFormUrl(r.seriesId); - ui.review.hidden = false; - draw(); + save(); } catch (err) { if (running?.signal.aborted) { markStage(currentStage, "reset"); @@ -306,6 +415,7 @@ export function mountApp(o: AppOptions): void { if (res.allFailed) status(ui.rehostStatus, `Every upload failed: ${res.failures[0]?.message ?? "unknown error"}`, true); else { const done = host === "direct" ? `${res.hosted.size} ready with their source URLs (not rehosted)` : `${res.hosted.size} rehosted`; + marks = { ...marks, rehosted: { at: new Date().toISOString(), host } }; status(ui.rehostStatus, `${done}${res.failures.length ? `, ${res.failures.length} failed (see the covers)` : ""}.`); } } catch (err) { @@ -320,7 +430,7 @@ export function mountApp(o: AppOptions): void { const bundle = o.session.bundle(); saveFile(zipStore(bundle.files), bundle.zipName, "application/zip", ui.download); status(ui.manualStatus, `Saved ${bundle.zipName} (${bundle.files.length - 1} covers and covers.txt).`); - o.onDownloaded?.(); + mark({ downloaded: new Date().toISOString() }); } catch (err) { status(ui.manualStatus, errorText(err), true); } @@ -363,6 +473,7 @@ export function mountApp(o: AppOptions): void { const outcome = await o.session.submit(t, prepared, host, hostOptions()); if (outcome.status === "saved") { status(ui.submitStatus, `Filed edit request #${outcome.result.submission_id} (${outcome.result.status}). A MangaBaka moderator will review it.`); + mark({ filed: { at: new Date().toISOString(), submissionId: outcome.result.submission_id } }); prepared = null; ui.confirm.hidden = true; } else { @@ -378,5 +489,14 @@ export function mountApp(o: AppOptions): void { } }); + ui.searchAgain.addEventListener("click", () => void run()); + ui.recentClear.addEventListener("click", async () => { + if (!o.history || !confirm("Clear the whole history? The saved covers are deleted from this browser.")) return; + await o.history.clear().catch(() => undefined); + currentId = null; + void refreshRecent(); + }); + drawHosts(); + void refreshRecent(); } diff --git a/src/web/history-db.ts b/src/web/history-db.ts new file mode 100644 index 0000000..cecf830 --- /dev/null +++ b/src/web/history-db.ts @@ -0,0 +1,60 @@ +import { summarize, type HistoryStore, type SavedSearch, type SearchSummary } from "../core/saved"; + +// Spec 002 §3.2: IndexedDB on the page's origin. `summaries` is what the list reads; `searches` holds the bytes. + +const DB = "mbseed"; +const SUMMARIES = "summaries"; +const SEARCHES = "searches"; + +const done = (req: IDBRequest): Promise => + new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error ?? new Error("IndexedDB request failed")); + }); + +const committed = (tx: IDBTransaction): Promise => + new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error ?? new Error("IndexedDB transaction failed")); + tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted")); + }); + +/** The page's history, or null when this browser won't give us IndexedDB (private windows, blocked storage). */ +export async function openHistory(factory: IDBFactory | undefined = globalThis.indexedDB): Promise { + if (!factory) return null; + let db: IDBDatabase; + try { + const req = factory.open(DB, 1); + req.onupgradeneeded = () => { + req.result.createObjectStore(SUMMARIES, { keyPath: "seriesId" }); + req.result.createObjectStore(SEARCHES, { keyPath: "seriesId" }); + }; + db = await done(req); + } catch { + return null; + } + const write = async (run: (summaries: IDBObjectStore, searches: IDBObjectStore) => void) => { + const tx = db.transaction([SUMMARIES, SEARCHES], "readwrite"); + run(tx.objectStore(SUMMARIES), tx.objectStore(SEARCHES)); + await committed(tx); + }; + return { + list: async () => { + const all = await done(db.transaction(SUMMARIES).objectStore(SUMMARIES).getAll() as IDBRequest); + return all.sort((a, b) => b.savedAt.localeCompare(a.savedAt)); + }, + get: async (id) => (await done(db.transaction(SEARCHES).objectStore(SEARCHES).get(id) as IDBRequest)) ?? null, + put: (s) => write((summaries, searches) => { + summaries.put(summarize(s)); + searches.put(s); + }), + remove: (id) => write((summaries, searches) => { + summaries.delete(id); + searches.delete(id); + }), + clear: () => write((summaries, searches) => { + summaries.clear(); + searches.clear(); + }), + }; +} diff --git a/src/web/main.ts b/src/web/main.ts index 8899e9d..c35b31b 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -2,6 +2,7 @@ import { Session } from "../core/session"; import { withPolicy } from "../core/transport"; import { mountApp } from "./app"; import { browserThemeDocument, followDarkReader } from "./dark-reader"; +import { openHistory } from "./history-db"; import { APP_HTML } from "./template"; import { localStore, pageTransport } from "./transport"; import type { ServerConfig } from "./view-model"; @@ -13,6 +14,7 @@ const config = await fetch("/api/config") .then((r) => (r.ok ? (r.json() as Promise) : null)) .catch(() => null); const store = localStore(); +const history = await openHistory(); const session = new Session({ transport: withPolicy(pageTransport(location.origin)), serverBase: location.origin, @@ -20,4 +22,4 @@ const session = new Session({ store, persistHostedUrls: false, }); -mountApp({ root: document, session, store, config, catboxNeedsServer: true }); +mountApp({ root: document, session, store, config, catboxNeedsServer: true, history }); diff --git a/src/web/recent.ts b/src/web/recent.ts new file mode 100644 index 0000000..c2d81d6 --- /dev/null +++ b/src/web/recent.ts @@ -0,0 +1,63 @@ +import type { SearchSummary } from "../core/saved"; +import { el } from "./render"; +import { markText, relativeTime, summaryLine } from "./view-model"; + +// Spec 002 §3.4: the History list on the left desk and the shelf of recent series on the start screen. + +export interface RecentHandlers { + open(seriesId: number): void; + remove(seriesId: number): void; +} + +const SHELF_SIZE = 6; +/** Thumbnail Blob URLs of the last render, revoked on the next one. */ +let thumbUrls: string[] = []; + +function thumb(s: SearchSummary): HTMLImageElement | HTMLSpanElement { + if (!s.thumb) return el("span", { className: "mbseed-recent-thumb is-blank" }); + const url = URL.createObjectURL(new Blob([s.thumb.bytes as Uint8Array], { type: s.thumb.contentType })); + thumbUrls.push(url); + return el("img", { className: "mbseed-recent-thumb", src: url, alt: "", decoding: "async" }); +} + +const CROSS = ``; + +function row(s: SearchSummary, current: boolean, now: Date, h: RecentHandlers): HTMLLIElement { + const marks = markText(s.marks); + const open = el("button", { type: "button", className: "mbseed-recent-open" }, + thumb(s), + el("span", { className: "mbseed-recent-text" }, + el("span", { className: "mbseed-recent-title", textContent: s.title }), + el("span", { className: "mbseed-recent-meta", textContent: `${relativeTime(s.savedAt, now)} · ${summaryLine(s)}` }), + ...(marks.length ? [el("span", { className: "mbseed-recent-marks", textContent: marks.join(" · ") })] : []))); + if (current) open.setAttribute("aria-current", "true"); + open.addEventListener("click", () => h.open(s.seriesId)); + const remove = el("button", { type: "button", className: "mbseed-recent-remove", title: "Remove from history", ariaLabel: `Remove ${s.title} from history` }); + remove.innerHTML = CROSS; + remove.addEventListener("click", () => h.remove(s.seriesId)); + return el("li", {}, open, remove); +} + +function shelfBook(s: SearchSummary, now: Date, h: RecentHandlers): HTMLButtonElement { + const book = el("button", { type: "button", className: "mbseed-recent-book", ariaLabel: `Open ${s.title}` }, + el("span", { className: "mbseed-recent-jacket" }, thumb(s)), + el("span", { className: "mbseed-recent-band" }, + el("span", { className: "mbseed-recent-title", textContent: s.title }), + el("span", { className: "mbseed-recent-meta", textContent: `${relativeTime(s.savedAt, now)} · ${s.fresh} new` }))); + book.addEventListener("click", () => h.open(s.seriesId)); + return book; +} + +export function renderRecent( + list: HTMLElement, + shelf: HTMLElement, + items: readonly SearchSummary[], + current: number | null, + now: Date, + h: RecentHandlers, +): void { + for (const url of thumbUrls) URL.revokeObjectURL(url); + thumbUrls = []; + list.replaceChildren(...items.map((s) => row(s, s.seriesId === current, now, h))); + shelf.replaceChildren(...items.slice(0, SHELF_SIZE).map((s) => shelfBook(s, now, h))); +} diff --git a/src/web/render.ts b/src/web/render.ts index 8f62cf8..28fb317 100644 --- a/src/web/render.ts +++ b/src/web/render.ts @@ -11,7 +11,7 @@ const ROLE: Readonly> = { /** Results already shelved once: the shelving motion plays on a new run, not on every edit. */ const shelved = new WeakSet(); -function el( +export function el( tag: K, props: Partial = {}, ...children: (Node | string)[] diff --git a/src/web/styles.css b/src/web/styles.css index e59a48a..e46d266 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -226,11 +226,63 @@ .mbseed-app .mbseed-warnings li { font-size: 12px; color: var(--mb-ink-2); padding-left: 12px; position: relative; overflow-wrap: anywhere; } .mbseed-app .mbseed-warnings li::before { content: ""; position: absolute; left: 0; top: 0.6em; width: 5px; height: 5px; background: var(--mb-kraft); } +/* ---------- history: a ruled list on the left desk (spec 002 §3.4) ---------- */ +.mbseed-app .mbseed-recent { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; } +.mbseed-app .mbseed-recent-list { + list-style: none; margin: 0; padding: 0; display: grid; max-height: 24rem; overflow: auto; + scrollbar-width: thin; scrollbar-color: var(--mb-kraft) transparent; +} +.mbseed-app .mbseed-recent-list li { display: flex; margin-top: -1px; border: 1px solid var(--mb-line); } +.mbseed-app .mbseed-recent-open { + flex: 1; min-width: 0; display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; align-items: center; + padding: 6px 8px; border: 0; background: transparent; color: var(--mb-ink-2); font: inherit; text-align: left; cursor: pointer; + transition: background-color 0.15s; +} +.mbseed-app .mbseed-recent-open:hover, .mbseed-app .mbseed-recent-open[aria-current="true"] { background: var(--mb-slip); } +.mbseed-app .mbseed-recent-list .mbseed-recent-thumb { + display: block; width: 30px; height: 40px; object-fit: contain; background: var(--mb-page); + outline: 1px solid var(--mb-frame); outline-offset: -1px; +} +.mbseed-app .mbseed-recent-text { display: grid; gap: 1px; min-width: 0; } +.mbseed-app .mbseed-recent-title { font-size: 13px; font-weight: 700; color: var(--mb-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.mbseed-app .mbseed-recent-meta { font-size: 11.5px; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.mbseed-app .mbseed-recent-marks { font-size: 11.5px; font-weight: 600; color: var(--mb-teal); } +.mbseed-app .mbseed-recent-remove { + flex: none; width: 28px; display: grid; place-items: center; padding: 0; + border: 0; border-left: 1px solid var(--mb-line); background: transparent; color: var(--mb-ink-2); cursor: pointer; + transition: background-color 0.15s, color 0.15s; +} +.mbseed-app .mbseed-recent-remove:hover { background: var(--mb-ink); color: var(--mb-page); } +.mbseed-app .mbseed-recent .mbseed-line:empty { display: none; } +.mbseed-app .mbseed-recent > .is-quiet { justify-self: start; width: auto; font-size: 12.5px; color: var(--mb-ink-2); } + /* ---------- empty state ---------- */ .mbseed-app .mbseed-empty { max-width: 46rem; } .mbseed-app .mbseed-empty h2 { font-size: clamp(22px, 3cqi, 30px); font-weight: 800; letter-spacing: -0.02em; margin: 24px 0 10px; text-wrap: balance; } .mbseed-app .mbseed-empty p { color: var(--mb-ink-2); max-width: 62ch; margin: 0 0 10px; font-size: 14px; } .mbseed-app .mbseed-empty strong { color: var(--mb-ink); } +/* Recently searched: the same books on the same kraft rail, smaller, each opening its history entry. */ +.mbseed-app .mbseed-recent-shelf-wrap { display: grid; gap: 14px; margin-top: 34px; } +.mbseed-app .mbseed-recent-shelf { display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); align-items: end; column-gap: 0; row-gap: 30px; } +.mbseed-app .mbseed-recent-book { + display: flex; flex-direction: column; justify-content: flex-end; min-width: 0; padding: 0 9px; + border: 0; border-bottom: 10px solid var(--mb-kraft); box-shadow: 0 2px 0 var(--mb-rail); + background: transparent; color: var(--mb-ink); font: inherit; text-align: left; cursor: pointer; +} +.mbseed-app .mbseed-recent-jacket { display: block; } +.mbseed-app .mbseed-recent-book .mbseed-recent-thumb { + display: block; width: 100%; height: auto; max-height: 160px; object-fit: contain; object-position: bottom; + outline: 1px solid var(--mb-frame); outline-offset: -1px; box-shadow: 0 8px 14px -8px var(--mb-shadow); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} +.mbseed-app .mbseed-recent-book .mbseed-recent-thumb.is-blank { height: 120px; background: var(--mb-panel); box-shadow: none; } +.mbseed-app .mbseed-recent-book:hover .mbseed-recent-thumb { transform: translateY(-4px); } +.mbseed-app .mbseed-recent-band { + position: relative; margin-top: -12px; display: grid; gap: 1px; padding: 5px 7px 6px; + background: var(--mb-slip); border: 1px solid var(--mb-line); border-top: 3px solid var(--mb-ink); +} +.mbseed-app .mbseed-recent-band .mbseed-recent-title { font-size: 12px; } +.mbseed-app .mbseed-recent-band .mbseed-recent-meta { color: var(--mb-ink-2); } /* ---------- review head ---------- */ .mbseed-app .mbseed-review-head { @@ -242,6 +294,8 @@ .mbseed-app .mbseed-views { margin-left: auto; display: flex; border: 1.5px solid var(--mb-ink); } .mbseed-app .mbseed-view { border: 0; background: transparent; color: var(--mb-ink); padding: 4px 12px; font: 700 12.5px var(--mb-sans); cursor: pointer; } .mbseed-app .mbseed-view[aria-pressed="true"] { background: var(--mb-ink); color: var(--mb-page); } +.mbseed-app .mbseed-found { margin: 8px 0 0; display: flex; align-items: baseline; gap: 4px 10px; flex-wrap: wrap; font-size: 13px; color: var(--mb-ink-2); } +.mbseed-app .mbseed-found .is-quiet { width: auto; color: var(--mb-ink); } .mbseed-app .mbseed-notice { margin: 12px 0 0; padding: 8px 12px; font-size: 13px; background: color-mix(in srgb, var(--mb-obi) 12%, var(--mb-page)); border-top: 2px solid var(--mb-obi); diff --git a/src/web/template.ts b/src/web/template.ts index fbef8ef..741de4c 100644 --- a/src/web/template.ts +++ b/src/web/template.ts @@ -39,6 +39,13 @@ export const APP_HTML = /* html */ `

Notes

    + + Install the userscript @@ -47,6 +54,10 @@ export const APP_HTML = /* html */ `

    Paste a MangaBaka series link and the series page on its source site, then press Find covers. Current covers come straight from the source; older ones from the Wayback Machine.

    Nothing is uploaded or sent to MangaBaka until you press a button for it.

    + +