Move to rp.jarviscli.dev with real per-plugin pages

This commit is contained in:
everestmcarthur 2026-08-01 11:00:23 -04:00
parent 6af091940f
commit 37ceb8e424
9 changed files with 218 additions and 23 deletions

53
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Bug report
description: Something in a plugin is broken or behaving wrong
labels: ["bug"]
body:
- type: dropdown
id: plugin
attributes:
label: Which plugin?
options:
- Staff Tags
- RoleColorEverywhere
- PronounDB
- Message Snippets
- Reminders
- Not sure / something else
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect, and what did you actually get?
placeholder: "I tried /snippet welcome and nothing was sent to the channel."
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
placeholder: |
1. Enable the plugin
2. Run /snippet welcome
3. ...
validations:
required: false
- type: input
id: discord-version
attributes:
label: Discord app version (Settings → About)
placeholder: "e.g. 338.4"
validations:
required: false
- type: textarea
id: logs
attributes:
label: Anything in the Revenge debugger/logs?
description: Settings → General → Debugger, if you have it enabled
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Core Revenge app issue (not a plugin)
url: https://github.com/revenge-mod/revenge-bundle/issues
about: If the problem is with Revenge itself rather than one of these plugins, report it there instead.

View file

@ -0,0 +1,29 @@
name: Feature request / new plugin idea
description: Something you want added to an existing plugin, or a new plugin entirely
labels: ["enhancement"]
body:
- type: dropdown
id: kind
attributes:
label: What kind of request is this?
options:
- New feature for an existing plugin
- A brand new plugin idea
validations:
required: true
- type: input
id: plugin
attributes:
label: Which plugin? (leave blank if this is a new plugin idea)
placeholder: "e.g. Staff Tags"
validations:
required: false
- type: textarea
id: description
attributes:
label: What do you want, and why?
description: The more concrete, the better - what would you actually do with it?
validations:
required: true

View file

@ -0,0 +1,55 @@
name: A plugin stopped working after a Discord update
description: The most common failure mode here - Discord renamed/removed something a plugin depends on
labels: ["bug", "discord-update"]
body:
- type: markdown
attributes:
value: |
Discord's internals change without notice, and plugins in this ecosystem patch those internals
directly - this is the single most common way something breaks here. Reports like this are
genuinely useful, thank you.
- type: dropdown
id: plugin
attributes:
label: Which plugin?
options:
- Staff Tags
- RoleColorEverywhere
- PronounDB
- Message Snippets
- Reminders
validations:
required: true
- type: dropdown
id: surface
attributes:
label: What stopped showing up / working?
description: Pick whichever's closest - helps narrow down which patch broke
options:
- Chat messages
- Member list
- Profile popout
- Channel header
- Typing indicator
- Voice channel
- A slash command
- Not sure
validations:
required: false
- type: input
id: discord-version
attributes:
label: Discord app version (Settings → About)
placeholder: "e.g. 338.4"
validations:
required: true
- type: textarea
id: worked-before
attributes:
label: Did this work before, and if so, roughly when did it stop?
validations:
required: false

View file

@ -2,11 +2,17 @@
A collection of plugins for [Revenge](https://github.com/revenge-mod/revenge-bundle), the Discord Android client mod. A collection of plugins for [Revenge](https://github.com/revenge-mod/revenge-bundle), the Discord Android client mod.
**Browse them at [rp.jarviscli.dev](https://rp.jarviscli.dev)** - searchable, with a full detail page per plugin.
## Installing a plugin ## Installing a plugin
1. Open Discord with Revenge installed, go to **Settings → Plugins**. Each plugin has two URLs:
2. Tap the `+` button and paste the plugin's install link (see below).
3. Enable it. - `rp.jarviscli.dev/<plugin>/` - a real page describing the plugin (features, commands, how it works)
- `rp.jarviscli.dev/<plugin>/install/` - the actual install link you paste into Revenge
To install: open Discord with Revenge installed, go to **Settings → Plugins**, tap `+`, and paste the
`/install/` link.
## Plugins ## Plugins
@ -14,16 +20,16 @@ A collection of plugins for [Revenge](https://github.com/revenge-mod/revenge-bun
| Plugin | Status | Description | Install link | | Plugin | Status | Description | Install link |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Staff Tags | Rebuilt | OWNER/ADMIN/STAFF/MOD-style tags next to members, computed from real server permissions. Per-tag text, colors, gradients, and visibility. | `https://everestmcarthur.github.io/revenge-plugins/staff-tags/` | | Staff Tags | Rebuilt | OWNER/ADMIN/STAFF/MOD-style tags next to members, computed from real server permissions. Per-tag text, colors, gradients, and visibility. | `https://rp.jarviscli.dev/staff-tags/install/` |
| RoleColorEverywhere | Revived | A member's top role color shown in mentions, the typing indicator, voice channel names, member list role headers, and optionally message text. | `https://everestmcarthur.github.io/revenge-plugins/role-color-everywhere/` | | RoleColorEverywhere | Revived | A member's top role color shown in mentions, the typing indicator, voice channel names, member list role headers, and optionally message text. | `https://rp.jarviscli.dev/role-color-everywhere/install/` |
| PronounDB | Revived | Shows a user's pronouns in their profile, if they've set them at pronoundb.org (nothing to configure in the plugin itself). | `https://everestmcarthur.github.io/revenge-plugins/pronoun-db/` | | PronounDB | Revived | Shows a user's pronouns in their profile, if they've set them at pronoundb.org (nothing to configure in the plugin itself). | `https://rp.jarviscli.dev/pronoun-db/install/` |
### Productivity ### Productivity
| Plugin | Status | Description | Install link | | Plugin | Status | Description | Install link |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Message Snippets | New | Save reusable text and send it with `/snippet <name>`. Manage snippets in-app or with `/snippet-save`, `/snippet-delete`, `/snippet-list`. | `https://everestmcarthur.github.io/revenge-plugins/message-snippets/` | | Message Snippets | New | Save reusable text and send it with `/snippet <name>`. Manage snippets in-app or with `/snippet-save`, `/snippet-delete`, `/snippet-list`. | `https://rp.jarviscli.dev/message-snippets/install/` |
| Reminders | New | `/remind 20m Walk the dog` - fires while Discord is running. Can't wake the app from fully closed (no native notification access from a JS plugin). | `https://everestmcarthur.github.io/revenge-plugins/reminders/` | | Reminders | New | `/remind 20m Walk the dog` - fires while Discord is running. Can't wake the app from fully closed (no native notification access from a JS plugin). | `https://rp.jarviscli.dev/reminders/install/` |
"Revived" means the plugin previously existed elsewhere, stopped working after a Discord/API update, and has been rebuilt here. "Revived" means the plugin previously existed elsewhere, stopped working after a Discord/API update, and has been rebuilt here.
"Rebuilt" means it's a from-scratch reimplementation of a previously-broken plugin with expanded features. "New" means "Rebuilt" means it's a from-scratch reimplementation of a previously-broken plugin with expanded features. "New" means
@ -58,7 +64,19 @@ npm install
npm run build npm run build
``` ```
Output is written to `dist/<plugin>/`. This runs both `build.mjs` (compiles every plugin in `plugins/*` into `dist/<id>/install/`) and
`scripts/generate-site.mjs` (builds `dist/plugins-data.json` from those manifests + `site/meta.json`,
and writes the website - a homepage at `dist/index.html` and a real page per plugin at `dist/<id>/index.html`).
## Releasing an update
1. Edit a plugin under `plugins/<id>/src/`.
2. `npm run build` locally and sanity-check the output in `dist/`.
3. `git add -A && git commit -m "..." && git push origin main`.
4. GitHub Actions rebuilds and redeploys to `rp.jarviscli.dev` automatically (usually under a minute) -
check progress at the repo's **Actions** tab.
There's no manual deploy step - anything merged to `main` goes live.
## Contributing ## Contributing

View file

@ -60,9 +60,11 @@ const plugins = [
esbuild({ minify: true }), esbuild({ minify: true }),
]; ];
// Each plugin's actual install target (manifest.json + index.js) lives at /<id>/install/, so that
// /<id>/ itself is free to be a real, bookmarkable HTML page describing the plugin (see generate-site.mjs).
for (const plug of await readdir("./plugins")) { for (const plug of await readdir("./plugins")) {
const manifest = JSON.parse(await readFile(`./plugins/${plug}/manifest.json`)); const manifest = JSON.parse(await readFile(`./plugins/${plug}/manifest.json`));
const outPath = `./dist/${plug}/index.js`; const outPath = `./dist/${plug}/install/index.js`;
try { try {
const bundle = await rollup({ const bundle = await rollup({
@ -90,7 +92,7 @@ for (const plug of await readdir("./plugins")) {
const toHash = await readFile(outPath); const toHash = await readFile(outPath);
manifest.hash = createHash("sha256").update(toHash).digest("hex"); manifest.hash = createHash("sha256").update(toHash).digest("hex");
manifest.main = "index.js"; manifest.main = "index.js";
await writeFile(`./dist/${plug}/manifest.json`, JSON.stringify(manifest)); await writeFile(`./dist/${plug}/install/manifest.json`, JSON.stringify(manifest));
console.log(`Successfully built ${manifest.name}!`); console.log(`Successfully built ${manifest.name}!`);
} catch (e) { } catch (e) {

View file

@ -1,6 +1,6 @@
import { readFile, writeFile, readdir, copyFile, stat } from "fs/promises"; import { readFile, writeFile, readdir, copyFile, stat } from "fs/promises";
const PAGES_BASE = "https://everestmcarthur.github.io/revenge-plugins"; const PAGES_BASE = "https://rp.jarviscli.dev";
const REPO_BASE = "https://github.com/everestmcarthur/revenge-plugins"; const REPO_BASE = "https://github.com/everestmcarthur/revenge-plugins";
const meta = JSON.parse(await readFile("./site/meta.json", "utf8")); const meta = JSON.parse(await readFile("./site/meta.json", "utf8"));
@ -12,7 +12,7 @@ for (const id of await readdir("./dist")) {
let manifest; let manifest;
try { try {
manifest = JSON.parse(await readFile(`${dir}/manifest.json`, "utf8")); manifest = JSON.parse(await readFile(`${dir}/install/manifest.json`, "utf8"));
} catch { } catch {
continue; // not a plugin output directory continue; // not a plugin output directory
} }
@ -33,7 +33,8 @@ for (const id of await readdir("./dist")) {
features: info.features ?? [], features: info.features ?? [],
commands: info.commands ?? [], commands: info.commands ?? [],
limitations: info.limitations ?? "", limitations: info.limitations ?? "",
installUrl: `${PAGES_BASE}/${id}/`, pageUrl: `${PAGES_BASE}/${id}/`,
installUrl: `${PAGES_BASE}/${id}/install/`,
sourceUrl: `${REPO_BASE}/tree/main/plugins/${id}`, sourceUrl: `${REPO_BASE}/tree/main/plugins/${id}`,
issueUrl: `${REPO_BASE}/issues/new?title=${encodeURIComponent(`[${manifest.name}] `)}` issueUrl: `${REPO_BASE}/issues/new?title=${encodeURIComponent(`[${manifest.name}] `)}`
}); });
@ -44,5 +45,12 @@ plugins.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeComp
await writeFile("./dist/plugins-data.json", JSON.stringify(plugins, null, 2)); await writeFile("./dist/plugins-data.json", JSON.stringify(plugins, null, 2));
await copyFile("./site/index.html", "./dist/index.html"); await copyFile("./site/index.html", "./dist/index.html");
await copyFile("./site/index.html", "./dist/404.html"); await copyFile("./site/index.html", "./dist/404.html");
await copyFile("./site/CNAME", "./dist/CNAME");
// Each plugin gets its own real, bookmarkable page at /<id>/ (not just a hash route) - it's the
// same SPA shell, which reads location.pathname on boot to know which plugin to show directly.
for (const p of plugins) {
await copyFile("./site/index.html", `./dist/${p.id}/index.html`);
}
console.log(`Generated site data for ${plugins.length} plugin(s).`); console.log(`Generated site data for ${plugins.length} plugin(s).`);

1
site/CNAME Normal file
View file

@ -0,0 +1 @@
rp.jarviscli.dev

View file

@ -96,6 +96,12 @@
from { transform: translate3d(0,0,0) scale(1); } from { transform: translate3d(0,0,0) scale(1); }
to { transform: translate3d(-2%, 2%, 0) scale(1.08); } to { transform: translate3d(-2%, 2%, 0) scale(1.08); }
} }
.hero-spotlight {
position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0;
background: radial-gradient(280px circle at var(--sx, 50%) var(--sy, 50%), color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%);
transition: opacity 0.25s ease;
}
.hero:hover .hero-spotlight { opacity: 1; }
.hero-inner { position: relative; z-index: 1; } .hero-inner { position: relative; z-index: 1; }
.mark { .mark {
display: inline-flex; display: inline-flex;
@ -294,6 +300,7 @@
<header class="hero" id="hero"> <header class="hero" id="hero">
<div class="hero-mesh" aria-hidden="true"></div> <div class="hero-mesh" aria-hidden="true"></div>
<div class="hero-spotlight" id="heroSpotlight" aria-hidden="true"></div>
<div class="wrap hero-inner"> <div class="wrap hero-inner">
<div class="mark"> <div class="mark">
<svg viewBox="0 0 32 32" fill="none"><path d="M9 12l7-5 7 5v9l-7 5-7-5z" stroke="currentColor" stroke-width="2"/><circle cx="16" cy="16" r="3" fill="currentColor"/></svg> <svg viewBox="0 0 32 32" fill="none"><path d="M9 12l7-5 7 5v9l-7 5-7-5z" stroke="currentColor" stroke-width="2"/><circle cx="16" cy="16" r="3" fill="currentColor"/></svg>
@ -471,7 +478,7 @@
'<div class="cta">View details &rarr;</div>'; '<div class="cta">View details &rarr;</div>';
card.addEventListener("click", function () { card.addEventListener("click", function () {
location.hash = "#/plugin/" + p.id; navigateToPlugin(p.id);
}); });
grid.appendChild(card); grid.appendChild(card);
@ -508,6 +515,13 @@
controlsEl.classList.toggle("stuck", window.scrollY > 8); controlsEl.classList.toggle("stuck", window.scrollY > 8);
}); });
var heroSpotlight = document.getElementById("heroSpotlight");
heroEl.addEventListener("mousemove", function (e) {
var rect = heroEl.getBoundingClientRect();
heroSpotlight.style.setProperty("--sx", (e.clientX - rect.left) + "px");
heroSpotlight.style.setProperty("--sy", (e.clientY - rect.top) + "px");
});
/* ---------- Detail view ---------- */ /* ---------- Detail view ---------- */
function renderDetail(p) { function renderDetail(p) {
@ -571,7 +585,7 @@
actions.appendChild(issueLink); actions.appendChild(issueLink);
document.getElementById("backLink").addEventListener("click", function () { document.getElementById("backLink").addEventListener("click", function () {
history.pushState(null, "", location.pathname + location.search); history.pushState(null, "", "/");
showGrid(); showGrid();
}); });
} }
@ -597,16 +611,26 @@
document.title = "revenge-plugins"; document.title = "revenge-plugins";
} }
function routeFromHash() { /**
var match = location.hash.match(/^#\/plugin\/([\w-]+)/); * Each plugin has a real static page at /<id>/ (a copy of this same file), so a direct visit or
if (match) { * refresh works without any server-side routing - this just avoids a full reload when navigating
* from within an already-loaded page.
*/
function navigateToPlugin(id) {
history.pushState(null, "", "/" + id + "/");
showDetail(id);
}
function routeFromLocation() {
var match = location.pathname.match(/^\/([\w-]+)\/?$/);
if (match && match[1]) {
showDetail(match[1]); showDetail(match[1]);
} else { } else {
showGrid(); showGrid();
} }
} }
window.addEventListener("hashchange", routeFromHash); window.addEventListener("popstate", routeFromLocation);
/* ---------- Command palette ---------- */ /* ---------- Command palette ---------- */
@ -659,7 +683,7 @@
var plugin = paletteMatches[i]; var plugin = paletteMatches[i];
if (!plugin) return; if (!plugin) return;
closePalette(); closePalette();
location.hash = "#/plugin/" + plugin.id; navigateToPlugin(plugin.id);
} }
paletteInput.addEventListener("input", function (e) { renderPalette(e.target.value); }); paletteInput.addEventListener("input", function (e) { renderPalette(e.target.value); });
@ -692,14 +716,14 @@
/* ---------- Boot ---------- */ /* ---------- Boot ---------- */
fetch("https://everestmcarthur.github.io/revenge-plugins/plugins-data.json") fetch("https://rp.jarviscli.dev/plugins-data.json")
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (data) { .then(function (data) {
state.plugins = data; state.plugins = data;
renderStats(); renderStats();
renderChips(); renderChips();
render(); render();
routeFromHash(); routeFromLocation();
}) })
.catch(function () { .catch(function () {
count.textContent = ""; count.textContent = "";