diff --git a/.claude/settings.json b/.claude/settings.json index aef5004..09d7aef 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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:*)", diff --git a/.gemini/settings.json b/.gemini/settings.json index dd3a43a..fcf4046 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -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)", diff --git a/AGENTS.md b/AGENTS.md index 454a5ed..1915d87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/.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 diff --git a/docs/PERMISSIONS.md b/docs/PERMISSIONS.md index 597c2f3..b5ebb51 100644 --- a/docs/PERMISSIONS.md +++ b/docs/PERMISSIONS.md @@ -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. | diff --git a/docs/TODO.md b/docs/TODO.md index 06a2567..360e04e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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? diff --git a/src/userscript/main.ts b/src/userscript/main.ts new file mode 100644 index 0000000..4dce1a5 --- /dev/null +++ b/src/userscript/main.ts @@ -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: ///. */ +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 { + 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 { + 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 = ` +
+
Seed covers +
+
Settings + + Only needed to rehost on that server. litterbox and catbox work without one. +
+ ${APP_HTML} +
`; + document.body.append(panel); + + const serverInput = shadow.querySelector("#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 = ``; +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); diff --git a/src/web/app.ts b/src/web/app.ts index 61c869f..1da3c5f 100644 --- a/src/web/app.ts +++ b/src/web/app.ts @@ -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 { diff --git a/src/web/styles.css b/src/web/styles.css index 11dbec1..cf6c306 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -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; }