feat(userscript): Seed covers panel on MangaBaka series pages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
162ab4afb0
commit
df7e1bcd42
@@ -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:*)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
@@ -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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user