feat: fetch roles at runtime and prepare solver
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
src/generated/roles.ts
|
||||||
|
.env
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Tweaks for WoV Assassins Convention",
|
"description": "Tweaks for WoV Assassins Convention",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "esbuild src/content.ts src/popup.ts src/background.ts --bundle --outdir=dist --platform=browser --target=chrome120",
|
"build": "node scripts/build.mjs",
|
||||||
"watch": "esbuild src/content.ts src/popup.ts src/background.ts --bundle --outdir=dist --platform=browser --target=chrome120 --watch",
|
"watch": "node scripts/build.mjs --watch",
|
||||||
"dev": "pnpm watch"
|
"dev": "pnpm watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
63
scripts/build.mjs
Normal file
63
scripts/build.mjs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -26,3 +26,24 @@ chrome.runtime.onMessage.addListener((message: SimulateClickMessage) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const checkForUpdate = async (): Promise<void> => {
|
||||||
|
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);
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ const onKeyDown = (event: KeyboardEvent): void => {
|
|||||||
const init = async (): Promise<void> => {
|
const init = async (): Promise<void> => {
|
||||||
const result = await chrome.storage.local.get(STORAGE_KEY);
|
const result = await chrome.storage.local.get(STORAGE_KEY);
|
||||||
const state = result[STORAGE_KEY] as State | undefined;
|
const state = result[STORAGE_KEY] as State | undefined;
|
||||||
enabled = state?.enabled ?? true;
|
enabled = state?.keybinds ?? true;
|
||||||
|
|
||||||
document.addEventListener("keydown", onKeyDown, { capture: true });
|
document.addEventListener("keydown", onKeyDown, { capture: true });
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener((changes) => {
|
chrome.storage.onChanged.addListener((changes) => {
|
||||||
if (STORAGE_KEY in changes) {
|
if (STORAGE_KEY in changes) {
|
||||||
const next = changes[STORAGE_KEY].newValue as State;
|
const next = changes[STORAGE_KEY].newValue as State;
|
||||||
enabled = next.enabled;
|
enabled = next.keybinds;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/env.d.ts
vendored
Normal file
2
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare const __GIT_REVISION__: string;
|
||||||
|
declare const __GITEA_API__: string;
|
||||||
0
src/generated/.gitkeep
Normal file
0
src/generated/.gitkeep
Normal file
@@ -16,17 +16,24 @@ h1 {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 16px;
|
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%;
|
width: 100%;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -36,14 +43,16 @@ p {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
|
font-family: monospace;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toggle.on {
|
.row.on {
|
||||||
background: #22c55e;
|
background: #22c55e;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toggle.off {
|
.row.off {
|
||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>WoV Tweaks</title>
|
<title>WAC</title>
|
||||||
<link rel="stylesheet" href="popup.css">
|
<link rel="stylesheet" href="popup.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Assassins Convention Tweaks</h1>
|
<h1>Assassins Convention Tweaks</h1>
|
||||||
<button id="toggle"></button>
|
<div id="update-banner" style="display:none">[!] Update available — reinstall the extension</div>
|
||||||
|
<div id="tweaks"></div>
|
||||||
<script src="../dist/popup.js"></script>
|
<script src="../dist/popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
48
src/popup.ts
48
src/popup.ts
@@ -1,34 +1,56 @@
|
|||||||
import { State, STORAGE_KEY } from "./types";
|
import { State, STORAGE_KEY } from "./types";
|
||||||
|
|
||||||
const DEFAULT_STATE: State = {
|
const defaultState: State = {
|
||||||
enabled: true,
|
keybinds: true,
|
||||||
|
solver: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getState = async (): Promise<State> => {
|
const getState = async (): Promise<State> => {
|
||||||
const result = await chrome.storage.local.get(STORAGE_KEY);
|
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<void> => {
|
const setState = async (state: State): Promise<void> => {
|
||||||
await chrome.storage.local.set({ [STORAGE_KEY]: state });
|
await chrome.storage.local.set({ [STORAGE_KEY]: state });
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = (btn: HTMLButtonElement, enabled: boolean): void => {
|
const createRow = (label: string, initial: boolean, onChange: (next: boolean) => void): HTMLButtonElement => {
|
||||||
btn.textContent = enabled ? "ON" : "OFF";
|
let on = initial;
|
||||||
btn.className = enabled ? "on" : "off";
|
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<void> => {
|
const init = async (): Promise<void> => {
|
||||||
const btn = document.getElementById("toggle") as HTMLButtonElement;
|
const container = document.getElementById("tweaks")!;
|
||||||
const state = await getState();
|
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 current = await getState();
|
||||||
const next = { enabled: !current.enabled };
|
await setState({ ...current, keybinds: next });
|
||||||
await setState(next);
|
}));
|
||||||
render(btn, next.enabled);
|
|
||||||
});
|
container.appendChild(createRow("Solver", state.solver, async (next) => {
|
||||||
|
const current = await getState();
|
||||||
|
await setState({ ...current, solver: next });
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type State = {
|
export type State = {
|
||||||
enabled: boolean;
|
keybinds: boolean;
|
||||||
|
solver: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const STORAGE_KEY = "wov_assassins_convention_tweaks_state";
|
export const STORAGE_KEY = "wov_assassins_convention_tweaks_state";
|
||||||
|
|||||||
Reference in New Issue
Block a user