diff --git a/.gitignore b/.gitignore index b947077..74c7e88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/ dist/ +src/generated/roles.ts +.env diff --git a/package.json b/package.json index 96ebf52..6267d87 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "Tweaks for WoV Assassins Convention", "scripts": { - "build": "esbuild src/content.ts src/popup.ts src/background.ts --bundle --outdir=dist --platform=browser --target=chrome120", - "watch": "esbuild src/content.ts src/popup.ts src/background.ts --bundle --outdir=dist --platform=browser --target=chrome120 --watch", + "build": "node scripts/build.mjs", + "watch": "node scripts/build.mjs --watch", "dev": "pnpm watch" }, "devDependencies": { diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..d9cc9d9 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,63 @@ +import * as esbuild from "esbuild"; +import { execSync } from "child_process"; +import { writeFileSync, mkdirSync, readFileSync } from "fs"; + +// read .env file is present +try { + for (const line of readFileSync(".env", "utf8").split("\n")) { + const [key, ...rest] = line.split("="); + if (key && rest.length) { + process.env[key.trim()] = rest.join("=").trim(); + } + } +} catch {} + +const apiKey = process.env.WOV_API_KEY; +if (!process.env.WOV_API_KEY) { + throw new Error("WOV_API_KEY is not set"); +} + +let revision; +try { revision = execSync("git rev-parse HEAD").toString().trim(); } +catch { throw new Error("failed to get git revision"); } + +// Fetch roles +console.log("fetching roles... "); +const res = await fetch("https://api.wolvesville.com/roles", { headers: { "Authorization": `Bot ${apiKey}` } }); +const data = await res.json(); +if (!res.ok) { + throw new Error(`roles API error ${res.status}: ${JSON.stringify(data)}`); +} +if (!Array.isArray(data.roles) || data.roles.length === 0) { + throw new Error(`unexpected roles response: ${JSON.stringify(data)}`); +} +if (data.roles.some(r => typeof r.name !== "string")) { + throw new Error("some roles are missing a name field"); +} + +const code = `// Auto generated by scripts/build.mjs\nexport const ROLES: string[] = ${JSON.stringify(data.roles.map(r => r.name), null, 4)};\n`; + +mkdirSync("src/generated", { recursive: true }); +writeFileSync("src/generated/roles.ts", code); +console.log("done."); + +// Bundle +const options = { + entryPoints: ["src/content.ts", "src/popup.ts", "src/background.ts"], + bundle: true, + outdir: "dist", + platform: "browser", + target: "chrome120", + define: { + __GIT_REVISION__: JSON.stringify(revision), + __GITEA_API__: JSON.stringify("https://git.pihkaal.me/api/v1/repos/pihkaal/wov-assassins-convention-tweaks"), + }, +}; + +if (process.argv.includes("--watch")) { + const ctx = await esbuild.context(options); + await ctx.watch(); + console.log("watching..."); +} else { + await esbuild.build(options); +} diff --git a/src/background.ts b/src/background.ts index 9fdf9e2..d864891 100644 --- a/src/background.ts +++ b/src/background.ts @@ -26,3 +26,24 @@ chrome.runtime.onMessage.addListener((message: SimulateClickMessage) => { }); }); }); + +const checkForUpdate = async (): Promise => { + try { + const res = await fetch(`${__GITEA_API__}/branches/main`); + const data = await res.json(); + const latest: string = data.commit.id; + const needsUpdate = latest !== __GIT_REVISION__; + + await chrome.storage.local.set({ needsUpdate }); + + if (needsUpdate) { + chrome.action.setBadgeText({ text: "!" }); + chrome.action.setBadgeBackgroundColor({ color: "#ef4444" }); + } else { + chrome.action.setBadgeText({ text: "" }); + } + } catch {} +}; + +chrome.runtime.onStartup.addListener(checkForUpdate); +chrome.runtime.onInstalled.addListener(checkForUpdate); diff --git a/src/content.ts b/src/content.ts index 3251807..c22e80c 100644 --- a/src/content.ts +++ b/src/content.ts @@ -35,14 +35,14 @@ const onKeyDown = (event: KeyboardEvent): void => { const init = async (): Promise => { const result = await chrome.storage.local.get(STORAGE_KEY); const state = result[STORAGE_KEY] as State | undefined; - enabled = state?.enabled ?? true; + enabled = state?.keybinds ?? true; document.addEventListener("keydown", onKeyDown, { capture: true }); chrome.storage.onChanged.addListener((changes) => { if (STORAGE_KEY in changes) { const next = changes[STORAGE_KEY].newValue as State; - enabled = next.enabled; + enabled = next.keybinds; } }); }; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..82c3b7c --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,2 @@ +declare const __GIT_REVISION__: string; +declare const __GITEA_API__: string; diff --git a/src/generated/.gitkeep b/src/generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/popup.css b/src/popup.css index 52403cc..ed03529 100644 --- a/src/popup.css +++ b/src/popup.css @@ -16,17 +16,24 @@ h1 { font-size: 0.95rem; font-weight: 700; letter-spacing: 0.02em; - margin-bottom: 6px; -} - -p { - font-size: 0.75rem; - color: #94a3b8; - line-height: 1.4; margin-bottom: 16px; } -#toggle { +#update-banner { + font-family: monospace; + font-size: 0.8rem; + font-weight: 700; + color: #fbbf24; + margin-bottom: 12px; +} + +#tweaks { + display: flex; + flex-direction: column; + gap: 8px; +} + +.row { width: 100%; padding: 9px; border: none; @@ -36,14 +43,16 @@ p { cursor: pointer; letter-spacing: 0.05em; transition: background 0.15s; + font-family: monospace; + text-align: left; } -#toggle.on { +.row.on { background: #22c55e; color: #fff; } -#toggle.off { +.row.off { background: #3f3f46; color: #94a3b8; } diff --git a/src/popup.html b/src/popup.html index e8145cd..fb7b352 100644 --- a/src/popup.html +++ b/src/popup.html @@ -2,12 +2,13 @@ - WoV Tweaks + WAC

Assassins Convention Tweaks

- + +
diff --git a/src/popup.ts b/src/popup.ts index 25dbc29..66adc76 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -1,34 +1,56 @@ import { State, STORAGE_KEY } from "./types"; -const DEFAULT_STATE: State = { - enabled: true, +const defaultState: State = { + keybinds: true, + solver: true, }; const getState = async (): Promise => { const result = await chrome.storage.local.get(STORAGE_KEY); - return (result[STORAGE_KEY] as State | undefined) ?? DEFAULT_STATE; + return (result[STORAGE_KEY] as State | undefined) ?? defaultState; }; const setState = async (state: State): Promise => { await chrome.storage.local.set({ [STORAGE_KEY]: state }); }; -const render = (btn: HTMLButtonElement, enabled: boolean): void => { - btn.textContent = enabled ? "ON" : "OFF"; - btn.className = enabled ? "on" : "off"; +const createRow = (label: string, initial: boolean, onChange: (next: boolean) => void): HTMLButtonElement => { + let on = initial; + const btn = document.createElement("button"); + + const render = () => { + btn.textContent = `[${on ? "ON" : "OFF"}] ${label}`; + btn.className = `row ${on ? "on" : "off"}`; + }; + + render(); + btn.addEventListener("click", () => { + on = !on; + render(); + onChange(on); + }); + + return btn; }; const init = async (): Promise => { - const btn = document.getElementById("toggle") as HTMLButtonElement; + const container = document.getElementById("tweaks")!; const state = await getState(); - render(btn, state.enabled); - btn.addEventListener("click", async () => { + const { needsUpdate } = await chrome.storage.local.get("needsUpdate"); + if (needsUpdate) { + document.getElementById("update-banner")!.style.display = "block"; + } + + container.appendChild(createRow("Keybinds", state.keybinds, async (next) => { const current = await getState(); - const next = { enabled: !current.enabled }; - await setState(next); - render(btn, next.enabled); - }); + await setState({ ...current, keybinds: next }); + })); + + container.appendChild(createRow("Solver", state.solver, async (next) => { + const current = await getState(); + await setState({ ...current, solver: next }); + })); }; document.addEventListener("DOMContentLoaded", init); diff --git a/src/types.ts b/src/types.ts index 04daacd..d0bb79c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ export type State = { - enabled: boolean; + keybinds: boolean; + solver: boolean; }; export const STORAGE_KEY = "wov_assassins_convention_tweaks_state";