A hand-built collection of plugins for Revenge, the Discord Android client mod - rebuilt from scratch where the old ecosystem broke, and net-new where nothing existed yet.
+
+
+
+
diff --git a/better-timestamps/install/index.js b/better-timestamps/install/index.js
new file mode 100644
index 0000000..a4ecd9c
--- /dev/null
+++ b/better-timestamps/install/index.js
@@ -0,0 +1 @@
+(function(o,g,r,y,a,i,h,p){"use strict";function v(e,t,s,n){try{return s()}catch(c){return n.error(`[${e}] Failed to apply the "${t}" patch, that surface will be skipped: ${c}`),function(){}}}function M(e,t,s){const n=Object.entries(s).map(function([c,C]){return v(e,c,C,t)});return function(){return n.forEach(function(c){return c()})}}function u(e,t=r.storage.selected){try{switch(t){case"calendar":return e.calendar();case"relative":return e.fromNow();case"iso":return e.toISOString();case"custom":return e.format(r.storage.customFormat||"dddd, MMMM Do YYYY, h:mm:ss a");default:return e.calendar()}}catch{return e?.calendar?.()??String(e)}}const l=y.findByName("RowManager",!1);function R(){if(!l?.prototype?.generate)return function(){};const e=[];return e.push(i.before("generate",l.prototype,function([t]){try{t.rowType===1?(r.storage.separateMessages&&(t.isFirst=!0),t.message.__customTimestamp=u(t.message.timestamp)):t.rowType==="day"&&(t.text=u(a.moment(t.text,"LL")))}catch{}})),e.push(i.after("generate",l.prototype,function([t],s){try{if(t.rowType!==1)return;t.message?.__customTimestamp&&s?.message?.state==="SENT"&&s.message.timestamp&&(s.message.timestamp=t.message.__customTimestamp)}catch{}})),function(){return e.forEach(function(t){return t()})}}const{ScrollView:b,View:w}=a.ReactNative;function E({children:e}){return a.React.createElement(b,{style:{flex:1}},e,a.React.createElement(w,{style:{height:24}}))}const{Text:F}=a.ReactNative,{FormSection:m,FormRow:k,FormDivider:Y,FormSwitchRow:S,FormInput:T}=p.Forms,d=[{label:"Calendar",key:"calendar"},{label:"Relative",key:"relative"},{label:"ISO 8601",key:"iso"},{label:"Custom",key:"custom"}];function _(){return h.useProxy(r.storage),a.React.createElement(E,null,a.React.createElement(m,{title:"Format"},d.map(function(e,t){const s=r.storage.selected===e.key;let n="";try{n=u(a.moment(),e.key)}catch{n=""}return a.React.createElement(a.React.Fragment,{key:e.key},a.React.createElement(k,{label:e.label,subLabel:n,trailing:s?a.React.createElement(F,{style:{fontSize:16}},"\u2713"):void 0,onPress:function(){r.storage.selected=e.key}}),e.key==="custom"&&s&&a.React.createElement(T,{title:"Custom format (moment.js tokens)",placeholder:"dddd, MMMM Do YYYY, h:mm:ss a",value:r.storage.customFormat,onChange:function(c){r.storage.customFormat=c}}),t!==d.length-1&&a.React.createElement(Y,null))})),a.React.createElement(m,{title:"Layout"},a.React.createElement(S,{label:"Always show name & avatar",subLabel:"Shows the username, avatar, and timestamp on every message instead of grouping consecutive messages",value:!!r.storage.separateMessages,onValueChange:function(e){r.storage.separateMessages=e}})))}let f=function(){};var L={onLoad:function(){r.storage.selected??="calendar",r.storage.customFormat??="dddd, MMMM Do YYYY, h:mm:ss a",r.storage.separateMessages??=!1,f=M("BetterTimestamps",g.logger,{"message & day timestamps":R})},onUnload:function(){return f()},settings:_};return o.default=L,Object.defineProperty(o,"__esModule",{value:!0}),o})({},vendetta,vendetta.plugin,vendetta.metro,vendetta.metro.common,vendetta.patcher,vendetta.storage,vendetta.ui.components);
diff --git a/better-timestamps/install/manifest.json b/better-timestamps/install/manifest.json
new file mode 100644
index 0000000..189546e
--- /dev/null
+++ b/better-timestamps/install/manifest.json
@@ -0,0 +1 @@
+{"name":"BetterTimestamps","description":"Replace message and day-divider timestamps with calendar-relative, fully relative, ISO 8601, or a custom moment.js format.","authors":[{"name":"everestmcarthur"},{"name":"nexpid"}],"main":"index.js","hash":"6ec03be5d13360e6caf33cf37895f04ce8cb9e549440a4ec60a79c10c582f2a3"}
\ No newline at end of file
diff --git a/copy-role-color/index.html b/copy-role-color/index.html
new file mode 100644
index 0000000..d3dce5e
--- /dev/null
+++ b/copy-role-color/index.html
@@ -0,0 +1,736 @@
+
+
+
+
+
+revenge-plugins
+
+
+
+
+
+
+
+
+
+
+
+
+ Revenge Plugins
+
+
Discord, your way.
+
A hand-built collection of plugins for Revenge, the Discord Android client mod - rebuilt from scratch where the old ecosystem broke, and net-new where nothing existed yet.
+
+
+
+
diff --git a/copy-role-color/install/index.js b/copy-role-color/install/index.js
new file mode 100644
index 0000000..bef3176
--- /dev/null
+++ b/copy-role-color/install/index.js
@@ -0,0 +1 @@
+(function(a,i,l,p,s,d,f,y){"use strict";function g(o,c,t,r){try{return t()}catch(e){return r.error(`[${o}] Failed to apply the "${c}" patch, that surface will be skipped: ${e}`),function(){}}}function h(o,c,t){const r=Object.entries(t).map(function([e,n]){return g(o,e,n,c)});return function(){return r.forEach(function(e){return e()})}}function b(){const o=l.findByName("ThemedRolePill",!1);return o?p.after("default",o,function(c,t){try{if(!t?.props?.onPress)return;const r=s.findInReactTree(t,function(n){return n?.props?.roleColor}),e=s.findInReactTree(t,function(n){return n?.props?.style?.[0]?.borderRadius&&typeof n?.props?.style?.[1]?.backgroundColor=="string"&&n.props.style[1].backgroundColor.startsWith("#")})?.props?.style?.[1]?.backgroundColor??r?.props?.roleColor;if(!e)return;t.props.onLongPress=function(){d.clipboard.setString(e),y.showToast("Copied role color to clipboard",f.getAssetIDByName("ic_message_copy"))}}catch{}}):function(){}}let u=function(){};var m={onLoad:function(){u=h("CopyRoleColor",i.logger,{"role pill long-press":b})},onUnload:function(){return u()}};return a.default=m,Object.defineProperty(a,"__esModule",{value:!0}),a})({},vendetta,vendetta.metro,vendetta.patcher,vendetta.utils,vendetta.metro.common,vendetta.ui.assets,vendetta.ui.toasts);
diff --git a/copy-role-color/install/manifest.json b/copy-role-color/install/manifest.json
new file mode 100644
index 0000000..24f5ec9
--- /dev/null
+++ b/copy-role-color/install/manifest.json
@@ -0,0 +1 @@
+{"name":"CopyRoleColor","description":"Long-press a role pill (member list, role list) to copy its color as hex.","authors":[{"name":"everestmcarthur"},{"name":"MSMA"}],"main":"index.js","hash":"e8448ac839287147e8c5b63198b2a40b8a85a2d7e9aab8f48acf93e6ed5c00c6"}
\ No newline at end of file
diff --git a/key-inspector/index.html b/key-inspector/index.html
new file mode 100644
index 0000000..d3dce5e
--- /dev/null
+++ b/key-inspector/index.html
@@ -0,0 +1,736 @@
+
+
+
+
+
+revenge-plugins
+
+
+
+
+
+
+
+
+
+
+
+
+ Revenge Plugins
+
+
Discord, your way.
+
A hand-built collection of plugins for Revenge, the Discord Android client mod - rebuilt from scratch where the old ecosystem broke, and net-new where nothing existed yet.
+
+
+
+
diff --git a/key-inspector/install/index.js b/key-inspector/install/index.js
new file mode 100644
index 0000000..0fccb77
--- /dev/null
+++ b/key-inspector/install/index.js
@@ -0,0 +1,6 @@
+(function(d,e,n,s,g,o){"use strict";const{ScrollView:b,View:N}=e.ReactNative;function R({children:t}){return e.React.createElement(b,{style:{flex:1}},t,e.React.createElement(N,{style:{height:24}}))}const{View:P,Text:C}=e.ReactNative;function S({children:t,style:i}){return e.React.createElement(P,{style:[{marginHorizontal:16,marginVertical:8,padding:10,borderRadius:8,borderWidth:1,borderColor:"rgba(128,128,128,0.25)"},i]},e.React.createElement(C,{style:{fontSize:12.5,lineHeight:18,opacity:.75}},t))}const{TouchableOpacity:T,Text:E}=e.ReactNative;function f({label:t,onPress:i,disabled:r,style:u}){return e.React.createElement(T,{disabled:r,onPress:i,style:[{backgroundColor:"#5865F2",borderRadius:8,padding:10,alignItems:"center",opacity:r?.5:1},u]},e.React.createElement(E,{style:{color:"white",fontWeight:"600"}},t))}const v=[{label:'findByProps("computePermissions", "canEveryoneRole")',run:function(){return n.findByProps("computePermissions","canEveryoneRole")}},{label:'findByProps("getBotLabel")',run:function(){return n.findByProps("getBotLabel")}},{label:'findByName("DisplayName", false)',run:function(){return n.findByName("DisplayName",!1)}},{label:'findByName("HeaderName", false)',run:function(){return n.findByName("HeaderName",!1)}},{label:'findByName("getTagProperties", false)',run:function(){return n.findByName("getTagProperties",!1)}},{label:'findByTypeNameAll("UserRow")',run:function(){return n.findByTypeNameAll("UserRow")}},{label:'findByStoreName("ThemeStore")',run:function(){return n.findByStoreName("ThemeStore")}},{label:'findByStoreName("GuildMemberStore")',run:function(){return n.findByStoreName("GuildMemberStore")}},{label:'findByStoreName("GuildStore")',run:function(){return n.findByStoreName("GuildStore")}},{label:'findByStoreName("ChannelStore")',run:function(){return n.findByStoreName("ChannelStore")}},{label:'findByStoreName("UserStore")',run:function(){return n.findByStoreName("UserStore")}},{label:'findByStoreName("RelationshipStore")',run:function(){return n.findByStoreName("RelationshipStore")}},{label:'findByName("RowManager", false)',run:function(){return n.findByName("RowManager",!1)}},{label:'findByProps("TYPING_WRAPPER_HEIGHT")',run:function(){return n.findByProps("TYPING_WRAPPER_HEIGHT")}},{label:'findByName("VoiceUserConnected", false)',run:function(){return n.findByName("VoiceUserConnected",!1)}},{label:"find(resolveSemanticColor via default.internal)",run:function(){return n.find(function(t){return t?.default?.internal?.resolveSemanticColor})}},{label:"find(resolveSemanticColor via meta)",run:function(){return n.find(function(t){return t?.meta?.resolveSemanticColor})}},{label:"find(UserProfile)",run:function(){return n.find(function(t){return t?.type?.name==="UserProfile"})}},{label:'findByName("UserProfileSection", false)',run:function(){return n.findByName("UserProfileSection",!1)}},{label:'findByProps("sendMessage", "sendBotMessage")',run:function(){return n.findByProps("sendMessage","sendBotMessage")}},{label:'findByProps("openLazy", "hideActionSheet")',run:function(){return n.findByProps("openLazy","hideActionSheet")}},{label:'findByProps("push", "pushLazy", "pop")',run:function(){return n.findByProps("push","pushLazy","pop")}},{label:'findByProps("getRenderCloseButton")',run:function(){return n.findByProps("getRenderCloseButton")}},{label:'findByProps("getHeaderCloseButton")',run:function(){return n.findByProps("getHeaderCloseButton")}},{label:'findByName("Navigator")',run:function(){return n.findByName("Navigator")}},{label:'findByName("ThemedRolePill", false)',run:function(){return n.findByName("ThemedRolePill",!1)}},{label:'findByProps("saveProfileChanges")',run:function(){return n.findByProps("saveProfileChanges")}},{label:'findByProps("useAvatarColors")',run:function(){return n.findByProps("useAvatarColors")}},{label:'findByProps("useThemeContext")',run:function(){return n.findByProps("useThemeContext")}},{label:'findByProps("getProfileTheme")',run:function(){return n.findByProps("getProfileTheme")}},{label:'findByProps("ThemeContextProvider")',run:function(){return n.findByProps("ThemeContextProvider")}},{label:"semanticColors.HEADER_SECONDARY",run:function(){return o.semanticColors?.HEADER_SECONDARY}},{label:"semanticColors.BACKGROUND_ACCENT",run:function(){return o.semanticColors?.BACKGROUND_ACCENT}},{label:"semanticColors.BACKGROUND_PRIMARY",run:function(){return o.semanticColors?.BACKGROUND_PRIMARY}},{label:"semanticColors.BACKGROUND_FLOATING",run:function(){return o.semanticColors?.BACKGROUND_FLOATING}},{label:"semanticColors.BUTTON_OUTLINE_BRAND_BORDER_ACTIVE",run:function(){return o.semanticColors?.BUTTON_OUTLINE_BRAND_BORDER_ACTIVE}},{label:"semanticColors.TEXT_NORMAL",run:function(){return o.semanticColors?.TEXT_NORMAL}}];function m(t){if(t==null)return"NOT FOUND";if(typeof t=="object"){const i=Object.keys(t);return`found (${i.length} key${i.length===1?"":"s"})`}return`found (${typeof t})`}function $(){return v.map(function({label:t,run:i}){try{const r=i();return{label:t,found:r!=null,detail:m(r)}}catch(r){return{label:t,found:!1,detail:`threw: ${r}`}}})}function O(t){const i=t.filter(function(r){return!r.found});return[`Key Inspector report - ${new Date().toISOString()}`,`${t.length} checked, ${i.length} missing`,"",...t.map(function(r){return`[${r.found?"OK":"MISSING"}] ${r.label} -> ${r.detail}`}),"",`semanticColors keys (${Object.keys(o.semanticColors??{}).length}):`,...Object.keys(o.semanticColors??{}).sort(),"",`rawColors keys (${Object.keys(o.rawColors??{}).length}):`,...Object.keys(o.rawColors??{}).sort()].join(`
+`)}const{View:p,Text:A}=e.ReactNative,{FormSection:B,FormInput:h}=g.Forms;function w(){const[t,i]=e.React.useState(null);return e.React.createElement(B,{title:"Full diagnostic scan"},e.React.createElement(p,{style:{paddingHorizontal:16,paddingVertical:8}},e.React.createElement(f,{label:"Run scan & copy full report",onPress:function(){const r=$(),u=r.filter(function(y){return!y.found}).length;e.clipboard.setString(O(r)),i(`${r.length} checked, ${u} missing - full report copied`),s.showToast(`${u} missing out of ${r.length} - report copied`,void 0)}}),t&&e.React.createElement(A,{style:{marginTop:8,fontSize:12.5,opacity:.7}},t)))}function _(){const[t,i]=e.React.useState(""),[r,u]=e.React.useState(""),y=function(){const a=t.split(",").map(function(l){return l.trim()}).filter(Boolean);if(a.length)try{const l=n.findByProps(...a);if(!l){e.clipboard.setString(`findByProps(${a.join(", ")}) -> not found`),s.showToast("No module found - copied that result",void 0);return}const c=Object.keys(l).sort();e.clipboard.setString(`findByProps(${a.join(", ")}) -> found, keys:
+${c.join(`
+`)}`),s.showToast(`Found it - copied ${c.length} key(s)`,void 0)}catch(l){e.clipboard.setString(`Error searching for ${a.join(", ")}: ${l}`),s.showToast("Search failed - copied the error instead",void 0)}},D=function(){const a=r.trim();if(a)try{const l=n.findByName(a,!1);if(!l){e.clipboard.setString(`findByName("${a}") -> not found`),s.showToast("No component found - copied that result",void 0);return}const c=Object.keys(l).sort();e.clipboard.setString(`findByName("${a}") -> found, keys:
+${c.join(`
+`)}`),s.showToast(`Found it - copied ${c.length} key(s)`,void 0)}catch(l){e.clipboard.setString(`Error searching for "${a}": ${l}`),s.showToast("Search failed - copied the error instead",void 0)}};return e.React.createElement(B,{title:"One-off search"},e.React.createElement(p,{style:{paddingHorizontal:16,paddingVertical:8}},e.React.createElement(h,{title:"findByProps - comma-separated prop names",placeholder:"e.g. sendMessage, sendBotMessage",value:t,onChange:i}),e.React.createElement(f,{label:"Search & copy result",style:{marginTop:8,marginBottom:16},onPress:y}),e.React.createElement(h,{title:"findByName - component name",placeholder:"e.g. ThemedRolePill",value:r,onChange:u}),e.React.createElement(f,{label:"Search & copy result",style:{marginTop:8},onPress:D})))}function k(){return e.React.createElement(R,null,e.React.createElement(w,null),e.React.createElement(_,null),e.React.createElement(S,null,"The full scan checks every internal lookup this repo's plugins depend on (plus a few for plugins currently being fixed) in one pass, and includes the complete list of semanticColors/rawColors keys at the end. Paste the report back into chat when reporting a bug - it tells us exactly what's missing on your Discord version."))}var U={settings:k};return d.default=U,Object.defineProperty(d,"__esModule",{value:!0}),d})({},vendetta.metro.common,vendetta.metro,vendetta.ui.toasts,vendetta.ui.components,vendetta.ui);
diff --git a/key-inspector/install/manifest.json b/key-inspector/install/manifest.json
new file mode 100644
index 0000000..c3f1ecf
--- /dev/null
+++ b/key-inspector/install/manifest.json
@@ -0,0 +1 @@
+{"name":"Key Inspector","description":"Dev utility: copies live Discord/Revenge color token keys to your clipboard, for diagnosing plugin crashes without needing a computer.","authors":[{"name":"everestmcarthur"}],"main":"index.js","hash":"a35903823b4ea650e0bf7390d770b7ae1a1992a70d22d064a22feb94fce2b56d"}
\ No newline at end of file
diff --git a/plugins-data.json b/plugins-data.json
index 7d9f7ce..7740713 100644
--- a/plugins-data.json
+++ b/plugins-data.json
@@ -24,6 +24,58 @@
"sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/pronoun-db",
"issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BPronounDB%5D%20"
},
+ {
+ "id": "better-timestamps",
+ "name": "BetterTimestamps",
+ "description": "Replace message and day-divider timestamps with calendar-relative, fully relative, ISO 8601, or a custom moment.js format.",
+ "authors": [
+ "everestmcarthur",
+ "nexpid"
+ ],
+ "category": "Moderation & Appearance",
+ "status": "revived",
+ "accent": "#EB459E",
+ "tagline": "Calendar-relative, fully relative, ISO 8601, or your own custom timestamp format.",
+ "note": "Previously broken (unguarded RowManager lookup). Rebuilt with the same crash-isolation pattern as the rest of this repo.",
+ "howItWorks": "Replaces both message timestamps and the \"day divider\" text in chat with your choice of format: calendar-relative (\"Yesterday at 3:04 PM\"), fully relative (\"2 hours ago\"), ISO 8601, or a fully custom moment.js format string. There's also a toggle to always show the name/avatar/timestamp on every message instead of Discord's default grouping of consecutive messages.",
+ "features": [
+ "Four built-in formats plus a custom moment.js format string",
+ "Live preview of each format in the settings screen",
+ "Applies to both message timestamps and day dividers",
+ "Optional: always show name/avatar/timestamp per message"
+ ],
+ "commands": [],
+ "limitations": "",
+ "pageUrl": "https://rp.jarviscli.dev/better-timestamps/",
+ "installUrl": "https://rp.jarviscli.dev/better-timestamps/install/",
+ "sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/better-timestamps",
+ "issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BBetterTimestamps%5D%20"
+ },
+ {
+ "id": "copy-role-color",
+ "name": "CopyRoleColor",
+ "description": "Long-press a role pill (member list, role list) to copy its color as hex.",
+ "authors": [
+ "everestmcarthur",
+ "MSMA"
+ ],
+ "category": "Moderation & Appearance",
+ "status": "revived",
+ "accent": "#1ABC9C",
+ "tagline": "Long-press a role pill to copy its color as hex.",
+ "note": "Previously broken (unguarded React-tree lookups that could throw on a shape mismatch). Rebuilt with defensive checks throughout.",
+ "howItWorks": "Long-press a role pill - in the member list, a role list, wherever Discord renders one as its own element - and the color gets copied to your clipboard as a hex code. Useful for designers, bot builders, or staff matching embed colors to a role.",
+ "features": [
+ "Works on any rendered role pill, not just one specific screen",
+ "Copies a ready-to-paste hex code (e.g. #5865F2)"
+ ],
+ "commands": [],
+ "limitations": "Depends on Discord's internal role pill component keeping a similar shape - guarded so a mismatch just means no long-press handler gets added, not a crash.",
+ "pageUrl": "https://rp.jarviscli.dev/copy-role-color/",
+ "installUrl": "https://rp.jarviscli.dev/copy-role-color/install/",
+ "sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/copy-role-color",
+ "issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BCopyRoleColor%5D%20"
+ },
{
"id": "role-color-everywhere",
"name": "RoleColorEverywhere",
@@ -153,6 +205,90 @@
"sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/reminders",
"issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BReminders%5D%20"
},
+ {
+ "id": "reverse-image-search",
+ "name": "Reverse Image Search",
+ "description": "/reverse-image-search - open an image link in Google Lens, Yandex, or TinEye.",
+ "authors": [
+ "everestmcarthur"
+ ],
+ "category": "Productivity",
+ "status": "new",
+ "accent": "#5865F2",
+ "tagline": "Open an image link in Google Lens, Yandex, or TinEye.",
+ "note": "Didn't exist in the Revenge/Vendetta plugin ecosystem before.",
+ "howItWorks": "Long-press an image in Discord, choose \"Copy Media Link\", then run this command with that link to open it in a reverse image search engine of your choice. Built purely on opening a URL - no Discord-internal patching at all, so it's about as low-risk as a plugin gets.",
+ "features": [
+ "Supports Google Lens, Yandex, and TinEye",
+ "Defaults to Google Lens if no engine is specified"
+ ],
+ "commands": [
+ {
+ "cmd": "/reverse-image-search [engine]",
+ "desc": "Opens the image in your browser's reverse image search"
+ }
+ ],
+ "limitations": "Needs a direct image URL, not a Discord attachment reference - use \"Copy Media Link\" first.",
+ "pageUrl": "https://rp.jarviscli.dev/reverse-image-search/",
+ "installUrl": "https://rp.jarviscli.dev/reverse-image-search/install/",
+ "sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/reverse-image-search",
+ "issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BReverse%20Image%20Search%5D%20"
+ },
+ {
+ "id": "urban-dictionary",
+ "name": "Urban Dictionary",
+ "description": "/urban - look up a definition from Urban Dictionary and send it to the channel.",
+ "authors": [
+ "everestmcarthur"
+ ],
+ "category": "Productivity",
+ "status": "revived",
+ "accent": "#F0B232",
+ "tagline": "Look up a definition without leaving Discord.",
+ "note": "Previously broken (dead proxy host). Rebuilt from scratch on the stable command API - no UI patching involved.",
+ "howItWorks": "Runs your term against Urban Dictionary's public API and posts the top result - definition, example, and the up/downvote count - directly into the channel. Built entirely on the same stable command API as Message Snippets and Reminders, so it doesn't carry the UI-patching fragility that most of this repo's revived plugins have to guard against.",
+ "features": [
+ "Posts the definition, an example, and vote counts",
+ "Truncates long entries so they stay readable in chat"
+ ],
+ "commands": [
+ {
+ "cmd": "/urban ",
+ "desc": "Looks up a term and posts the top Urban Dictionary result"
+ }
+ ],
+ "limitations": "Urban Dictionary's community submissions are what they are - results aren't moderated by this plugin.",
+ "pageUrl": "https://rp.jarviscli.dev/urban-dictionary/",
+ "installUrl": "https://rp.jarviscli.dev/urban-dictionary/install/",
+ "sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/urban-dictionary",
+ "issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BUrban%20Dictionary%5D%20"
+ },
+ {
+ "id": "key-inspector",
+ "name": "Key Inspector",
+ "description": "Dev utility: copies live Discord/Revenge color token keys to your clipboard, for diagnosing plugin crashes without needing a computer.",
+ "authors": [
+ "everestmcarthur"
+ ],
+ "category": "Utility",
+ "status": "new",
+ "accent": "#57F287",
+ "tagline": "One-tap diagnostic scan of every internal key this repo's plugins depend on - copy the report, no computer needed.",
+ "note": "Built to support fixing other broken plugins in this ecosystem entirely from a phone: most crashes come from a plugin assuming an internal key/component still exists after a Discord update, and this lets anyone check on-device and share the exact result.",
+ "howItWorks": "The main feature is a full scan: one button runs every internal lookup (findByProps, findByName, findByStoreName, specific semanticColors tokens) that this repo's plugins actually depend on, all in one pass, and copies a complete found/missing report plus the full current semanticColors and rawColors key lists to your clipboard. There's also a manual search section for anything not already in the scan list - type prop names or a component name and copy what findByProps/findByName returns for it. Nothing leaves your device; everything only touches your clipboard.",
+ "features": [
+ "Full scan: every known lookup, checked in one tap, with a copyable report",
+ "Manual findByProps(...) / findByName(...) search for anything else",
+ "Includes the complete semanticColors and rawColors key lists",
+ "No network requests, no data collection - clipboard only"
+ ],
+ "commands": [],
+ "limitations": "The full-scan list only covers lookups this repo's plugins currently use - it's meant to grow as more plugins get added or fixed, not to be exhaustive of everything Discord/Revenge expose.",
+ "pageUrl": "https://rp.jarviscli.dev/key-inspector/",
+ "installUrl": "https://rp.jarviscli.dev/key-inspector/install/",
+ "sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/key-inspector",
+ "issueUrl": "https://github.com/everestmcarthur/revenge-plugins/issues/new?title=%5BKey%20Inspector%5D%20"
+ },
{
"id": "view-raw",
"name": "ViewRaw",
@@ -176,7 +312,7 @@
"Crash fixed: no longer breaks the message long-press menu"
],
"commands": [],
- "limitations": "Licensed GPLv3 (inherited from the original), not the same terms as the rest of this repo - see plugins/view-raw/LICENSE.",
+ "limitations": "Licensed GPLv3 (inherited from the original), not the same terms as the rest of this repo - see plugins/view-raw/LICENSE. The long-press button relies on detecting Discord's internal action-sheet layout (not an official API) - the plugin's settings screen shows diagnostics if it ever stops appearing.",
"pageUrl": "https://rp.jarviscli.dev/view-raw/",
"installUrl": "https://rp.jarviscli.dev/view-raw/install/",
"sourceUrl": "https://github.com/everestmcarthur/revenge-plugins/tree/main/plugins/view-raw",
diff --git a/reverse-image-search/index.html b/reverse-image-search/index.html
new file mode 100644
index 0000000..d3dce5e
--- /dev/null
+++ b/reverse-image-search/index.html
@@ -0,0 +1,736 @@
+
+
+
+
+
+revenge-plugins
+
+
+
+
+
+
+
+
+
+
+
+
+ Revenge Plugins
+
+
Discord, your way.
+
A hand-built collection of plugins for Revenge, the Discord Android client mod - rebuilt from scratch where the old ecosystem broke, and net-new where nothing existed yet.
+
+
+
+
diff --git a/reverse-image-search/install/index.js b/reverse-image-search/install/index.js
new file mode 100644
index 0000000..0d090ea
--- /dev/null
+++ b/reverse-image-search/install/index.js
@@ -0,0 +1 @@
+(function(t,a,u,c){"use strict";const o=[{name:"google",url:function(e){return`https://lens.google.com/uploadbyurl?url=${encodeURIComponent(e)}`}},{name:"yandex",url:function(e){return`https://yandex.com/images/search?rpt=imageview&url=${encodeURIComponent(e)}`}},{name:"tineye",url:function(e){return`https://tineye.com/search?url=${encodeURIComponent(e)}`}}];function m(){return[a.registerCommand({name:"reverse-image-search",description:"Open an image URL in a reverse image search engine",options:[{name:"image-url",description:"Direct link to the image (long-press an image \u2192 Copy Media Link)",type:3,required:!0},{name:"engine",description:"google, yandex, or tineye (default: google)",type:3,required:!1}],execute:function(e){const i=e.find(function(n){return n.name==="image-url"})?.value;if(!i)return;const d=(e.find(function(n){return n.name==="engine"})?.value||"google").toLowerCase(),l=o.find(function(n){return n.name===d})??o[0];u.ReactNative.Linking.openURL(l.url(i)).catch(function(){c.showToast("Couldn't open the browser for this search",void 0)})}})]}let r=[];var s={onLoad:function(){r=m()},onUnload:function(){return r.forEach(function(e){return e()})}};return t.default=s,Object.defineProperty(t,"__esModule",{value:!0}),t})({},vendetta.commands,vendetta.metro.common,vendetta.ui.toasts);
diff --git a/reverse-image-search/install/manifest.json b/reverse-image-search/install/manifest.json
new file mode 100644
index 0000000..47eacaa
--- /dev/null
+++ b/reverse-image-search/install/manifest.json
@@ -0,0 +1 @@
+{"name":"Reverse Image Search","description":"/reverse-image-search - open an image link in Google Lens, Yandex, or TinEye.","authors":[{"name":"everestmcarthur"}],"main":"index.js","hash":"d56a50f02ef40f183afdd360dab58f37f397829f98f7dd0636ea8abe0d83409d"}
\ No newline at end of file
diff --git a/urban-dictionary/index.html b/urban-dictionary/index.html
new file mode 100644
index 0000000..d3dce5e
--- /dev/null
+++ b/urban-dictionary/index.html
@@ -0,0 +1,736 @@
+
+
+
+
+
+revenge-plugins
+
+
+
+
+
+
+
+
+
+
+
+
+ Revenge Plugins
+
+
Discord, your way.
+
A hand-built collection of plugins for Revenge, the Discord Android client mod - rebuilt from scratch where the old ecosystem broke, and net-new where nothing existed yet.
+
+
+
+
diff --git a/urban-dictionary/install/index.js b/urban-dictionary/install/index.js
new file mode 100644
index 0000000..53b056d
--- /dev/null
+++ b/urban-dictionary/install/index.js
@@ -0,0 +1,4 @@
+(function(i,d,c,r,f){"use strict";function a(t){return(t??"").replace(/[[\]]/g,"").trim()}async function m(t){const n=(await(await f.safeFetch(`https://api.urbandictionary.com/v0/define?term=${encodeURIComponent(t)}`)).json())?.list?.[0];if(n)return{word:n.word,definition:a(n.definition).slice(0,800),example:a(n.example).slice(0,400),thumbsUp:n.thumbs_up??0,thumbsDown:n.thumbs_down??0}}const l=3,u=c.findByProps("sendMessage","sendBotMessage");function h(){return[d.registerCommand({name:"urban",description:"Look up a term on Urban Dictionary",options:[{name:"term",description:"What to look up",type:l,required:!0}],execute:async function(t,n){const o=t.find(function(b){return b.name==="term"})?.value;if(!o)return;let e;try{e=await m(o)}catch{r.showToast(`Couldn't reach Urban Dictionary for "${o}"`,void 0);return}if(!e){r.showToast(`No definition found for "${o}"`,void 0);return}if(!u?.sendMessage){r.showToast("Couldn't send the definition - message action not found",void 0);return}const w=`**${e.word}** (\u{1F44D} ${e.thumbsUp} \u{1F44E} ${e.thumbsDown})
+${e.definition}`+(e.example?`
+
+*${e.example}*`:"");u.sendMessage(n.channel.id,{content:w})}})]}let s=[];var p={onLoad:function(){s=h()},onUnload:function(){return s.forEach(function(t){return t()})}};return i.default=p,Object.defineProperty(i,"__esModule",{value:!0}),i})({},vendetta.commands,vendetta.metro,vendetta.ui.toasts,vendetta.utils);
diff --git a/urban-dictionary/install/manifest.json b/urban-dictionary/install/manifest.json
new file mode 100644
index 0000000..84a2469
--- /dev/null
+++ b/urban-dictionary/install/manifest.json
@@ -0,0 +1 @@
+{"name":"Urban Dictionary","description":"/urban - look up a definition from Urban Dictionary and send it to the channel.","authors":[{"name":"everestmcarthur"}],"main":"index.js","hash":"8bbdb28697a555ff0bf0e5cd8cb8395bf77a3bd3a77921ecb314e8c66b52f315"}
\ No newline at end of file
diff --git a/view-raw/install/index.js b/view-raw/install/index.js
index 68e01db..0de2bd5 100644
--- a/view-raw/install/index.js
+++ b/view-raw/install/index.js
@@ -1,2 +1,2 @@
-(function(g,C,f,A,c,e,R,N){"use strict";function _(t){const n=JSON.parse(JSON.stringify(t));for(const o in n.author)switch(o){case"email":case"phone":case"mfaEnabled":case"hasBouncedEmail":delete n.author[o]}return n}const{View:T,Text:h}=e.ReactNative,l={key:"#7dd3fc",string:"#86efac",number:"#fbbf24",boolean:"#c4b5fd",null:"#9ca3af",punct:"#d1d5db"},k=/^(\s*)("(?:[^"\\]|\\.)*"\s*:\s*)?(.*)$/;function m(t){return t.startsWith('"')?l.string:/^(true|false)/.test(t)?l.boolean:/^null/.test(t)?l.null:/^-?\d/.test(t)?l.number:l.punct}function I({text:t,query:n}){const o=e.React.useMemo(function(){return t.split(`
-`)},[t]),r=(n||"").trim().toLowerCase();return e.React.createElement(T,null,o.map(function(u,B){const p=u.match(k),y=p?.[1]??"",i=p?.[2],s=p?.[3]??u,d=!!r&&u.toLowerCase().includes(r),a=!!r&&!d;return e.React.createElement(h,{key:B,selectable:!0,style:{fontFamily:"monospace",fontSize:12.5,lineHeight:18,opacity:a?.35:1,backgroundColor:d?"rgba(88,101,242,0.25)":"transparent"}},y,i?e.React.createElement(h,{style:{color:l.key}},i):null,e.React.createElement(h,{style:{color:m(s)}},s))}))}const{View:v,ScrollView:x,TextInput:L,Text:b}=e.ReactNative;function O({message:t}){const[n,o]=e.React.useState(""),r=e.React.useMemo(function(){return JSON.stringify(_(t),null,4)},[t.id]);return e.React.createElement(x,{style:{flex:1,marginHorizontal:13,marginVertical:10}},e.React.createElement(v,{style:{flexDirection:"row"}},e.React.createElement(R.Button,{style:{flex:1,marginRight:8,marginBottom:8},text:"Copy Content",color:"brand",size:"small",disabled:!t.content,onPress:function(){e.clipboard.setString(t.content),N.showToast("Copied content to clipboard",f.getAssetIDByName("toast_copy_link"))}}),e.React.createElement(R.Button,{style:{flex:1,marginBottom:8},text:"Copy JSON",color:"brand",size:"small",onPress:function(){e.clipboard.setString(r),N.showToast("Copied JSON to clipboard",f.getAssetIDByName("toast_copy_link"))}})),e.React.createElement(L,{value:n,onChangeText:o,placeholder:"Search keys or values\u2026",placeholderTextColor:"#888",autoCapitalize:"none",autoCorrect:!1,style:{borderWidth:1,borderColor:"rgba(128,128,128,0.35)",borderRadius:8,paddingHorizontal:10,paddingVertical:8,marginBottom:10,color:"#fff"}}),!!t.content&&e.React.createElement(v,{style:{marginBottom:12}},e.React.createElement(b,{style:{fontSize:11,opacity:.6,marginBottom:4,fontWeight:"700"}},"MESSAGE CONTENT"),e.React.createElement(b,{selectable:!0,style:{fontFamily:"monospace",fontSize:13}},t.content)),e.React.createElement(b,{style:{fontSize:11,opacity:.6,marginBottom:4,fontWeight:"700"}},"RAW DATA"),e.React.createElement(x,{horizontal:!0,showsHorizontalScrollIndicator:!0},e.React.createElement(I,{text:r,query:n})))}const w=c.findByProps("openLazy","hideActionSheet"),E=c.findByProps("push","pushLazy","pop"),$=c.findByProps("getRenderCloseButton")?.getRenderCloseButton??c.findByProps("getHeaderCloseButton")?.getHeaderCloseButton,D=c.findByName("Navigator")??c.findByProps("Navigator")?.Navigator,{FormRow:F,FormIcon:z}=R.Forms,H=C.before("openLazy",w,function([t,n,o]){const r=o?.message;n!=="MessageLongPressActionSheet"||!r||t.then(function(u){const B=C.after("default",u,function(p,y){e.React.useEffect(function(){return function(){B()}},[]);try{const i=function(){return e.React.createElement(D,{initialRouteName:"RawPage",goBackOnBackPress:!0,screens:{RawPage:{title:"ViewRaw",headerLeft:$?.(function(){return E.pop()}),render:function(){return e.React.createElement(O,{message:r})}}}})},s=A.findInReactTree(y,function(a){return Array.isArray(a)&&a[0]?.type?.name==="ActionSheetRowGroup"}),d=A.findInReactTree(y,function(a){return a?.[0]?.type?.name==="ButtonRow"});if(d)d.push(e.React.createElement(F,{label:"View Raw",leading:e.React.createElement(z,{style:{opacity:1},source:f.getAssetIDByName("ic_chat_bubble_16px")}),onPress:function(){w.hideActionSheet(),E.push(i)}}));else if(s&&s[1]){const a=s[1],P=a?.props?.children?.[0],V=P?.type,S=P?.props?.icon;if(!V||!S||!Array.isArray(a?.props?.children))console.log("[ViewRaw] Error: Could not find ActionSheetRow/icon shape");else{const J=e.React.createElement(V,{label:"View Raw",icon:{$$typeof:S.$$typeof,type:S.type,key:null,ref:null,props:{IconComponent:function(){return e.React.createElement(z,{style:{opacity:1},source:f.getAssetIDByName("ic_chat_bubble_32px")})}}},onPress:function(){w.hideActionSheet(),E.push(i)},key:"view-raw"});a.props.children.push(J)}}else console.log("[ViewRaw] Error: Could not find ActionSheet")}catch(i){console.log("[ViewRaw] Error: Failed to add View Raw button",i)}})})});var M={onUnload:function(){return H()}};return g.default=M,Object.defineProperty(g,"__esModule",{value:!0}),g})({},vendetta.patcher,vendetta.ui.assets,vendetta.utils,vendetta.metro,vendetta.metro.common,vendetta.ui.components,vendetta.ui.toasts);
+(function(w,P,p,E,s,e,h,S,x){"use strict";function m(t){const n=JSON.parse(JSON.stringify(t));for(const r in n.author)switch(r){case"email":case"phone":case"mfaEnabled":case"hasBouncedEmail":delete n.author[r]}return n}const{View:_,Text:B}=e.ReactNative,l={key:"#7dd3fc",string:"#86efac",number:"#fbbf24",boolean:"#c4b5fd",null:"#9ca3af",punct:"#d1d5db"},F=/^(\s*)("(?:[^"\\]|\\.)*"\s*:\s*)?(.*)$/;function L(t){return t.startsWith('"')?l.string:/^(true|false)/.test(t)?l.boolean:/^null/.test(t)?l.null:/^-?\d/.test(t)?l.number:l.punct}function O({text:t,query:n}){const r=e.React.useMemo(function(){return t.split(`
+`)},[t]),i=(n||"").trim().toLowerCase();return e.React.createElement(_,null,r.map(function(d,A){const R=d.match(F),y=R?.[1]??"",o=R?.[2],f=R?.[3]??d,c=!!i&&d.toLowerCase().includes(i),b=!!i&&!c;return e.React.createElement(B,{key:A,selectable:!0,style:{fontFamily:"monospace",fontSize:12.5,lineHeight:18,opacity:b?.35:1,backgroundColor:c?"rgba(88,101,242,0.25)":"transparent"}},y,o?e.React.createElement(B,{style:{color:l.key}},o):null,e.React.createElement(B,{style:{color:L(f)}},f))}))}const{View:z,ScrollView:T,TextInput:H,Text:v}=e.ReactNative;function W({message:t}){const[n,r]=e.React.useState(""),i=e.React.useMemo(function(){return JSON.stringify(m(t),null,4)},[t.id]);return e.React.createElement(T,{style:{flex:1,marginHorizontal:13,marginVertical:10}},e.React.createElement(z,{style:{flexDirection:"row"}},e.React.createElement(h.Button,{style:{flex:1,marginRight:8,marginBottom:8},text:"Copy Content",color:"brand",size:"small",disabled:!t.content,onPress:function(){e.clipboard.setString(t.content),S.showToast("Copied content to clipboard",p.getAssetIDByName("toast_copy_link"))}}),e.React.createElement(h.Button,{style:{flex:1,marginBottom:8},text:"Copy JSON",color:"brand",size:"small",onPress:function(){e.clipboard.setString(i),S.showToast("Copied JSON to clipboard",p.getAssetIDByName("toast_copy_link"))}})),e.React.createElement(H,{value:n,onChangeText:r,placeholder:"Search keys or values\u2026",placeholderTextColor:"#888",autoCapitalize:"none",autoCorrect:!1,style:{borderWidth:1,borderColor:"rgba(128,128,128,0.35)",borderRadius:8,paddingHorizontal:10,paddingVertical:8,marginBottom:10,color:"#fff"}}),!!t.content&&e.React.createElement(z,{style:{marginBottom:12}},e.React.createElement(v,{style:{fontSize:11,opacity:.6,marginBottom:4,fontWeight:"700"}},"MESSAGE CONTENT"),e.React.createElement(v,{selectable:!0,style:{fontFamily:"monospace",fontSize:13}},t.content)),e.React.createElement(v,{style:{fontSize:11,opacity:.6,marginBottom:4,fontWeight:"700"}},"RAW DATA"),e.React.createElement(T,{horizontal:!0,showsHorizontalScrollIndicator:!0},e.React.createElement(O,{text:i,query:n})))}function u(t,n=""){x.storage.lastDetection={strategy:t,detail:n,timestamp:Date.now()}}function J(){return x.storage.lastDetection}const{ScrollView:M,View:q}=e.ReactNative;function G({children:t}){return e.React.createElement(M,{style:{flex:1}},t,e.React.createElement(q,{style:{height:24}}))}const{View:j,Text:U}=e.ReactNative;function V({children:t,style:n}){return e.React.createElement(j,{style:[{marginHorizontal:16,marginVertical:8,padding:10,borderRadius:8,borderWidth:1,borderColor:"rgba(128,128,128,0.25)"},n]},e.React.createElement(U,{style:{fontSize:12.5,lineHeight:18,opacity:.75}},t))}const{TouchableOpacity:K,Text:Q}=e.ReactNative;function X({label:t,onPress:n,disabled:r,style:i}){return e.React.createElement(K,{disabled:r,onPress:n,style:[{backgroundColor:"#5865F2",borderRadius:8,padding:10,alignItems:"center",opacity:r?.5:1},i]},e.React.createElement(Q,{style:{color:"white",fontWeight:"600"}},t))}const{FormSection:Y,FormRow:Z}=h.Forms,ee={buttons:"Strategy 1 (button list) - working normally",actionSheetRow:"Strategy 2 (row group) - working normally",generic:"Strategy 3 (generic fallback) - the primary two shapes didn't match on this Discord build, but a broader fallback found a spot anyway",none:"None matched - the button couldn't be added on this Discord build"};function te(){const t=J();return e.React.createElement(G,null,e.React.createElement(Y,{title:"Diagnostics"},t?e.React.createElement(Z,{label:ee[t.strategy]??t.strategy,subLabel:`Last checked ${new Date(t.timestamp).toLocaleString()}${t.detail?` - ${t.detail}`:""}`}):e.React.createElement(V,null,"No data yet - long-press any message once, then come back here."),e.React.createElement(X,{style:{marginHorizontal:16,marginTop:8},label:"Copy diagnostics",disabled:!t,onPress:function(){e.clipboard.setString(JSON.stringify(t,null,2)),S.showToast("Copied - paste this if you're reporting an issue",void 0)}})),e.React.createElement(V,null,`"View Raw" is added to the message long-press menu by detecting Discord's internal action-sheet layout, which isn't an official API and can change between app versions. If it stops appearing for you, open this page after long-pressing a message and copy the diagnostics into a bug report - it'll say exactly which detection strategy ran.`))}const C=s.findByProps("openLazy","hideActionSheet"),N=s.findByProps("push","pushLazy","pop"),ne=s.findByProps("getRenderCloseButton")?.getRenderCloseButton??s.findByProps("getHeaderCloseButton")?.getHeaderCloseButton,ae=s.findByName("Navigator")??s.findByProps("Navigator")?.Navigator,{FormRow:re,FormIcon:$}=h.Forms;function D(t){return e.React.createElement(re,{label:"View Raw",leading:e.React.createElement($,{style:{opacity:1},source:p.getAssetIDByName("ic_chat_bubble_16px")}),onPress:function(){C.hideActionSheet(),N.push(t)}})}const ie=P.before("openLazy",C,function([t,n,r]){const i=r?.message;n!=="MessageLongPressActionSheet"||!i||t.then(function(d){const A=P.after("default",d,function(R,y){e.React.useEffect(function(){return function(){A()}},[]);try{const o=function(){return e.React.createElement(ae,{initialRouteName:"RawPage",goBackOnBackPress:!0,screens:{RawPage:{title:"ViewRaw",headerLeft:ne?.(function(){return N.pop()}),render:function(){return e.React.createElement(W,{message:i})}}}})},f=E.findInReactTree(y,function(a){return a?.[0]?.type?.name==="ButtonRow"}),c=E.findInReactTree(y,function(a){return Array.isArray(a)&&a[0]?.type?.name==="ActionSheetRowGroup"});if(f){f.push(D(o)),u("buttons");return}if(c&&c[1]){const a=c[1],g=a?.props?.children?.[0],I=g?.type,k=g?.props?.icon;if(I&&k&&Array.isArray(a?.props?.children)){const se=e.React.createElement(I,{label:"View Raw",icon:{$$typeof:k.$$typeof,type:k.type,key:null,ref:null,props:{IconComponent:function(){return e.React.createElement($,{style:{opacity:1},source:p.getAssetIDByName("ic_chat_bubble_32px")})}}},onPress:function(){C.hideActionSheet(),N.push(o)},key:"view-raw"});a.props.children.push(se),u("actionSheetRow");return}}const b=E.findInReactTree(y,function(a){return Array.isArray(a)&&a.length>0&&a.every(function(g){return typeof g?.props?.label=="string"&&typeof g?.props?.onPress=="function"})});if(Array.isArray(b)){b.push(D(o)),u("generic");return}console.log("[ViewRaw] Error: Could not find ActionSheet"),u("none")}catch(o){console.log("[ViewRaw] Error: Failed to add View Raw button",o),u("none",String(o))}})})});var oe={onUnload:function(){return ie()},settings:te};return w.default=oe,Object.defineProperty(w,"__esModule",{value:!0}),w})({},vendetta.patcher,vendetta.ui.assets,vendetta.utils,vendetta.metro,vendetta.metro.common,vendetta.ui.components,vendetta.ui.toasts,vendetta.plugin);
diff --git a/view-raw/install/manifest.json b/view-raw/install/manifest.json
index 255573c..797d544 100644
--- a/view-raw/install/manifest.json
+++ b/view-raw/install/manifest.json
@@ -1 +1 @@
-{"name":"ViewRaw","description":"View a message's raw JSON data - searchable, syntax-highlighted, and copyable. Fork of bwlok's ViewRaw with a crash fix and a nicer viewer.","authors":[{"name":"everestmcarthur"},{"name":"sapphire"},{"name":"Vendicated"},{"name":"Bwlok"}],"main":"index.js","hash":"d425f783f369101e889ecec73eebc9126cbf70cc3147de96f0b71370864e9aee"}
\ No newline at end of file
+{"name":"ViewRaw","description":"View a message's raw JSON data - searchable, syntax-highlighted, and copyable. Fork of bwlok's ViewRaw with a crash fix and a nicer viewer.","authors":[{"name":"everestmcarthur"},{"name":"sapphire"},{"name":"Vendicated"},{"name":"Bwlok"}],"main":"index.js","hash":"486cf24f3bb9d609a54bc5ae9ca6b3a0ba1d73dd08ee33a7a64da37f3bbc7305"}
\ No newline at end of file