feat(userscript): Seed covers panel on MangaBaka series pages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-15 01:40:04 +02:00
co-authored by Claude Opus 5.5
parent 162ab4afb0
commit df7e1bcd42
8 changed files with 122 additions and 6 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"allow": [
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", "Bash(git show:*)",
"Bash(git rev-parse:*)", "Bash(git add:*)", "Bash(git commit:*)",
"Bash(bun install --frozen-lockfile)", "Bash(bun test:*)", "Bash(bun run typecheck)"
"Bash(bun install --frozen-lockfile)", "Bash(bun test:*)", "Bash(bun run typecheck)", "Bash(bun run build)", "Bash(bun run build:web)", "Bash(bun run build:userscript)"
],
"ask": [
"Bash(git push:*)", "Bash(git merge:*)", "Bash(git rebase:*)", "Bash(git switch:*)",
+1 -1
View File
@@ -4,7 +4,7 @@
"run_shell_command(git status)", "run_shell_command(git diff)", "run_shell_command(git log)",
"run_shell_command(git show)", "run_shell_command(git branch --list)",
"run_shell_command(git rev-parse)", "run_shell_command(git add)", "run_shell_command(git commit)",
"run_shell_command(bun install --frozen-lockfile)", "run_shell_command(bun test)", "run_shell_command(bun run typecheck)"
"run_shell_command(bun install --frozen-lockfile)", "run_shell_command(bun test)", "run_shell_command(bun run typecheck)", "run_shell_command(bun run build)", "run_shell_command(bun run build:web)", "run_shell_command(bun run build:userscript)"
],
"exclude": [
"run_shell_command(git push --force)", "run_shell_command(git push -f)",
+1 -1
View File
@@ -11,7 +11,7 @@ Deeper context on demand: `docs/ARCHITECTURE.md`, `docs/DECISIONS.md`, `docs/TOD
Verified 2026-10-14 (see `docs/PERMISSIONS.md`):
`bun install --frozen-lockfile` · `bun test` · `bun test test/<file>.test.ts` · `bun run typecheck` ·
`bun run start` (server; env vars in `docs/ARCHITECTURE.md`).
`bun run build` (page + userscript into `dist/web/`) · `bun run start` (server; env vars in `docs/ARCHITECTURE.md`).
Use `bun`/`bunx` — never `npm`, `npx`, `node` or `vitest`.
## Workflow
+2 -1
View File
@@ -15,13 +15,14 @@ Run in this session. Only `worked` rows may appear in a tier, a tool file, or `A
| `bun install --frozen-lockfile` | worked |
| `bun test` | worked |
| `bun run typecheck` (`tsc --noEmit`) | worked |
| `bun run build` (`build:web`, `build:userscript`) | worked |
| `bun run start` | worked (local smoke run) |
## Tiers
| Tier | Commands | Why |
|---|---|---|
| Allow | `git status`, `git diff`, `git log`, `git show`, `git branch --list`, `git rev-parse`, `git add`, `git commit`; `bun install --frozen-lockfile`, `bun test`, `bun run typecheck` | Inspection and the dev loop; no prompt needed. |
| Allow | `git status`, `git diff`, `git log`, `git show`, `git branch --list`, `git rev-parse`, `git add`, `git commit`; `bun install --frozen-lockfile`, `bun test`, `bun run typecheck`, `bun run build` | Inspection and the dev loop; no prompt needed. |
| Ask | `git push`, `git merge`, `git rebase`, `git switch`, `git restore`, `git commit --amend`, `rm` (any); `bun add`, `bun remove`, `bun update`, `bun upgrade`; `bunx`, `npx` (download and run code, incl. `wrangler deploy`) | Leaves the machine, rewrites work, changes dependencies, or deploys — a human decides. |
| Deny | `git push --force` / `-f` / `--force-with-lease`, `git reset --hard`, `git clean`, `git add -f` / `--force`, `git commit --no-verify` / `-n`, `rm -r` / `-R` / `-rf` / `-fr` / `-Rf` / `-fR` / `--recursive`, `sudo`; reading `.env`, `.env.*`, `.dev.vars`, `secrets/**` | Destroys work, skips checks, or exposes secrets. |
+3
View File
@@ -32,6 +32,9 @@ Design: [specs/001-webtoons-covers.md](specs/001-webtoons-covers.md).
- [ ] Manual smoke test against Tower of God (9999 / title_no 95).
- [ ] Update README, `docs/ARCHITECTURE.md` and `PRODUCT.md`/`DESIGN.md` to match reality.
- [ ] Userscript: fill MangaBaka's cover form for users without a token (spec §4.9) — needs the logged-in
form's markup and a check that it accepts programmatic input (spec §7.5). The panel offers copy-and-paste instead.
## Verify with real services (once possible)
- [ ] Which of our servers and domain hosts it (`MBSEED_PUBLIC_URL`); where the covers folder lives.
- [ ] Does catbox accept uploads from our server?
+90
View File
@@ -0,0 +1,90 @@
import { Session } from "../core/session";
import { withPolicy } from "../core/transport";
import { mountApp } from "../web/app";
import styles from "../web/styles.css" with { type: "text" };
import { APP_HTML } from "../web/template";
import type { ServerConfig } from "../web/view-model";
import { gmStore, gmTransport } from "./gm";
const SERVER_KEY = "mbseed.server";
/** MangaBaka series pages: /<media type>/<id>/<slug>. */
const SERIES_PATH = /^\/(manga|manhwa|manhua|novel|oel|other)\/\d+(\/|$)/;
const store = gmStore(GM_getValue, GM_setValue, GM_deleteValue);
const transport = withPolicy(gmTransport(GM_xmlhttpRequest));
let panel: HTMLElement | null = null;
let panelPath: string | null = null;
async function loadConfig(server: string | null): Promise<ServerConfig | null> {
if (!server) return null;
try {
const res = await transport.fetch(`${server}/api/config`);
return res.ok ? ((await res.json()) as ServerConfig) : null;
} catch {
return null;
}
}
function closePanel(): void {
panel?.remove();
panel = null;
panelPath = null;
}
async function openPanel(): Promise<void> {
closePanel();
panelPath = location.pathname;
const server = store.get(SERVER_KEY);
panel = document.createElement("mbseed-panel");
panel.style.cssText = "position:fixed;top:16px;right:16px;bottom:16px;width:min(40rem,calc(100vw - 32px));overflow:auto;z-index:2147483647;";
const shadow = panel.attachShadow({ mode: "open" });
shadow.innerHTML = `<style>${styles}</style>
<div class="mbseed-panel">
<header class="mbseed-panel-bar"><strong>Seed covers</strong>
<button type="button" id="mbseed-close" class="mbseed-panel-close" aria-label="Close">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="2"/></svg>
</button></header>
<details class="mbseed-settings-panel"><summary>Settings</summary>
<label>mangabaka-seeder server (optional) <input id="mbseed-server" type="url" placeholder="https://mbseed.example.org"></label>
<small>Only needed to rehost on that server. litterbox and catbox work without one.</small>
</details>
${APP_HTML}
</div>`;
document.body.append(panel);
const serverInput = shadow.querySelector<HTMLInputElement>("#mbseed-server")!;
serverInput.value = server ?? "";
serverInput.addEventListener("change", () => {
const value = serverInput.value.trim().replace(/\/+$/, "");
if (value) store.set(SERVER_KEY, value);
else store.remove(SERVER_KEY);
void openPanel();
});
shadow.querySelector("#mbseed-close")!.addEventListener("click", closePanel);
const session = new Session({ transport, serverBase: server, catboxVia: "direct", store, persistHostedUrls: true });
mountApp({ root: shadow, session, store, config: await loadConfig(server), catboxNeedsServer: false, seriesLink: location.origin + location.pathname });
}
/** The launcher lives in its own shadow root so MangaBaka's styles can't reach it (DESIGN.md: the stamp). */
const launcherHost = document.createElement("mbseed-launcher");
launcherHost.style.cssText = "position:fixed;right:16px;bottom:16px;z-index:2147483646;";
const launcherRoot = launcherHost.attachShadow({ mode: "open" });
launcherRoot.innerHTML = `<style>
button { font: 800 12.5px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; letter-spacing: .14em; text-transform: uppercase;
background: #d6362f; color: #fffaf6; border: 0; padding: 11px 16px; cursor: pointer; box-shadow: 0 8px 14px -8px rgba(29,20,10,.5); }
button:hover { background: #8f1d18; }
button:focus-visible { outline: 2px solid #2f6f6a; outline-offset: 2px; }
</style><button type="button">Seed covers</button>`;
const launcher = launcherRoot.querySelector("button")!;
launcher.addEventListener("click", () => void openPanel());
document.body.append(launcherHost);
/** MangaBaka is a single-page app: follow client-side navigation. */
function sync(): void {
const onSeries = SERIES_PATH.test(location.pathname);
launcherHost.hidden = !onSeries;
if (panel && panelPath !== location.pathname) closePanel();
}
sync();
setInterval(sync, 1000);
+4 -1
View File
@@ -296,7 +296,10 @@ export function mountApp(o: AppOptions): void {
try {
const res = await o.session.rehost(host, hostOptions(), (done, total) => status(ui.rehostStatus, `Uploading… ${done}/${total}`));
if (res.allFailed) status(ui.rehostStatus, `Every upload failed: ${res.failures[0]?.message ?? "unknown error"}`, true);
else status(ui.rehostStatus, `${res.hosted.size} rehosted${res.failures.length ? `, ${res.failures.length} failed (see the covers)` : ""}.`);
else {
const done = host === "direct" ? `${res.hosted.size} ready with their source URLs (not rehosted)` : `${res.hosted.size} rehosted`;
status(ui.rehostStatus, `${done}${res.failures.length ? `, ${res.failures.length} failed (see the covers)` : ""}.`);
}
} catch (err) {
status(ui.rehostStatus, errorText(err), true);
} finally {
+20 -1
View File
@@ -126,7 +126,7 @@
}
.mbseed-app .mbseed-settings { grid-template-columns: minmax(0, 1fr); }
.mbseed-app .mbseed-setting > span { flex: none; }
.mbseed-app .mbseed-setting > select { flex: 1 1 auto; min-width: 0; max-width: 9.5rem; text-overflow: ellipsis; }
.mbseed-app .mbseed-setting > select { flex: 1 1 auto; min-width: 0; max-width: 11rem; text-overflow: ellipsis; }
.mbseed-app .mbseed-setting.is-stacked { flex-direction: column; align-items: stretch; gap: 3px; }
.mbseed-app .mbseed-setting.is-stacked input {
padding: 5px 8px; border: 1px solid var(--mb-line); background: var(--mb-slip); color: var(--mb-ink); font: 13px var(--mb-mono);
@@ -366,3 +366,22 @@
/* ---------- userscript panel chrome ---------- */
.mbseed-panel { background: var(--mb-page); color: var(--mb-ink); font: 14px/1.5 var(--mb-sans); border: 1px solid var(--mb-frame); box-shadow: 0 18px 40px -18px var(--mb-shadow); min-height: 100%; }
.mbseed-panel *, .mbseed-panel *::before, .mbseed-panel *::after { box-sizing: border-box; }
.mbseed-panel .mbseed-panel-bar {
position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px; background: var(--mb-panel); border-bottom: 3px solid var(--mb-ink);
font-size: 14px; font-weight: 800; letter-spacing: -0.01em;
}
.mbseed-panel .mbseed-panel-close {
display: grid; place-items: center; width: 30px; height: 30px; padding: 0; cursor: pointer;
border: 1.5px solid var(--mb-ink); background: transparent; color: var(--mb-ink);
}
.mbseed-panel .mbseed-panel-close:hover { background: var(--mb-ink); color: var(--mb-page); }
.mbseed-panel .mbseed-settings-panel { padding: 10px 16px; border-bottom: 1px solid var(--mb-line); background: var(--mb-panel); font-size: 13px; }
.mbseed-panel .mbseed-settings-panel summary { cursor: pointer; font-weight: 700; }
.mbseed-panel .mbseed-settings-panel label { display: grid; gap: 3px; margin-top: 8px; color: var(--mb-ink-2); }
.mbseed-panel .mbseed-settings-panel input {
padding: 5px 8px; border: 1px solid var(--mb-line); background: var(--mb-slip); color: var(--mb-ink); font: 13px var(--mb-mono);
}
.mbseed-panel .mbseed-settings-panel small { display: block; margin-top: 4px; color: var(--mb-ink-2); }
.mbseed-panel :focus-visible { outline: 2px solid var(--mb-teal); outline-offset: 2px; }