-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
add chrome extension popup #3760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
|
aislop |
64f3636 to
c4bcd26
Compare
|
this should be ready, @paigely i played with the style a bit you might want to take a look |
looks great!! |
| document.querySelector("#clear-css").addEventListener("click", async () => { | ||
| if ( | ||
| confirm( | ||
| "This will disable QuickCSS, any added themes and will remove all online themes. Do you want to continue?" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't actually do what it claims to do
| if (!rawSettings) return; | ||
| const settings = JSON.parse(rawSettings); | ||
| for (const name in settings.plugins) { | ||
| settings.plugins[name].enabled = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this, bad user experience. We should just add a safe mode
also marking settings as dirty (i.e. syncing to cloud) makes it even worse because this is definitely not what the user wants
| }); | ||
| document | ||
| .querySelector("#import-settings") | ||
| .addEventListener("click", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't like all this essentially duplicated from Vencord code, it hurts maintainability drastically. These should be in Vencord's source and exported on Vencord's global object
| remote: await (async () => { | ||
| let remote = process.env.VENCORD_REMOTE; | ||
| if (!remote) { | ||
| const res = await promisify(exec)( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this shouldn't be here, it should be in the same place as the git hash
| buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]), | ||
| buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]), | ||
| buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png", "icon-popup.webp", ...["html", "css", "js"].map(e => `popup.${e}`)]), | ||
| buildExtension("firefox-unpacked", ["background.js", "content.js", "manifest.json", "icon.png", "icon-popup.webp", ...["html", "css", "js"].map(e => `popup.${e}`)]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't do this cursed map
also since you deleted the firefox specific manifest, the background script is now completely unused and firefox has no csp bypass anymore
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very bad layout. You want grid, not flex.
<div class="button-grid">
<button>One</button>
<button>Two</button>
<button>Three</button>
<button>Four</button>
<button class="full-width">Five</button>
</div>.button-grid {
display: grid;
gap: 1em;
grid-template-columns: repeat(2, 1fr);
.full-width {
grid-column: 1 / -1;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of the class, you could even do this to do it automatically:
.button-grid {
display: grid;
gap: 1em;
grid-template-columns: repeat(2, 1fr);
> :last-child:nth-child(odd) {
grid-column: 1 / -1;
}
}| active: true, | ||
| lastFocusedWindow: true | ||
| }); | ||
| if (!tab.url) return undefined; // not a discord tab since it doesn't have the permissions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (!tab.url) return undefined; // not a discord tab since it doesn't have the permissions | |
| if (!tab.url) return null; // not a discord tab since it doesn't have the permissions |
| const api = globalThis.browser || globalThis.chrome; | ||
|
|
||
| async function getDiscordTab() { | ||
| let [tab] = await chrome.tabs.query({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does this use chrome instead of the api variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it's vibecoded
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HORROR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it's vibecoded
true
| @@ -0,0 +1,269 @@ | |||
| const api = globalThis.browser || globalThis.chrome; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use something like this then always refer to browser
| const api = globalThis.browser || globalThis.chrome; | |
| if (typeof browser === "undefined") { | |
| var browser = chrome; | |
| } | |
|
Other than that very cute idea and ui! |
|
abandonware |
|
This is not just abandonware — this is straight up abandonedware |
|
but soontm vee |
|
do copilot review @Vendicated |
|
@copilot finish this pr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a Chrome extension popup interface for Vencord Web, providing users with quick access to settings management and information about their installation. The popup displays version info, git commit hash, and action buttons for common tasks like importing/exporting settings, disabling plugins, and resetting themes.
Key Changes:
- Added popup UI with HTML, CSS, and JavaScript for managing Vencord settings
- Updated build script to generate and include metadata.json with version and repository information
- Modified manifest.json to include popup configuration and necessary permissions
- Removed manifestv2.json in favor of unified manifest.json for both Chrome and Firefox
Reviewed changes
Copilot reviewed 6 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| browser/popup.html | Popup HTML structure with version display and action buttons |
| browser/popup.css | Styling for the popup with gruvbox-inspired color scheme |
| browser/popup.js | Popup functionality including settings import/export, plugin management, and theme reset |
| browser/icon-popup.webp | New popup icon asset |
| browser/icon.png | Updated extension icon |
| scripts/build/buildWeb.mjs | Build script updated to generate metadata.json and include popup files in extension packages |
| browser/manifest.json | Added popup action and scripting permission |
| browser/manifestv2.json | Removed deprecated manifest v2 file |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const api = globalThis.browser || globalThis.chrome; | ||
|
|
||
| async function getDiscordTab() { | ||
| let [tab] = await chrome.tabs.query({ |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent API usage: The function uses chrome.tabs.query but the global API reference is stored as api which could be either browser or chrome. Consider using api.tabs.query for consistency with the established pattern.
| let [tab] = await chrome.tabs.query({ | |
| let [tab] = await api.tabs.query({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TRUEEE I TOLD YOU
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: () => { | ||
| const rawSettings = localStorage.getItem("VencordSettings"); | ||
| if (!rawSettings) return; | ||
| const settings = JSON.parse(rawSettings); | ||
| settings.useQuickCss = false; | ||
| localStorage.setItem( | ||
| "VencordSettings", | ||
| JSON.stringify(settings) | ||
| ); | ||
| localStorage.setItem("Vencord_settingsDirty", "true"); | ||
|
|
||
| window.location.reload(); | ||
| }, | ||
| args: [] | ||
| }); | ||
| } | ||
| }); | ||
| document | ||
| .querySelector("#disable-plugins") | ||
| .addEventListener("click", async () => { | ||
| if ( | ||
| confirm( | ||
| "This will disable all plugins and reload Discord. Do you want to continue?" | ||
| ) | ||
| ) { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: () => { | ||
| const rawSettings = | ||
| localStorage.getItem("VencordSettings"); | ||
| if (!rawSettings) return; | ||
| const settings = JSON.parse(rawSettings); | ||
| for (const name in settings.plugins) { | ||
| settings.plugins[name].enabled = false; | ||
| } | ||
| localStorage.setItem( | ||
| "VencordSettings", | ||
| JSON.stringify(settings) | ||
| ); | ||
| localStorage.setItem("Vencord_settingsDirty", "true"); | ||
|
|
||
| window.location.reload(); | ||
| }, | ||
| args: [] | ||
| }); | ||
| } | ||
| }); | ||
| document.querySelector("#reset").addEventListener("click", async () => { | ||
| if ( | ||
| prompt( | ||
| 'This will clear all your Vencord settings in this browser, including plugins, themes and QuickCSS! Make sure that you have a backup before continuing. To continue, type "YES".' | ||
| ) === "YES" | ||
| ) { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: () => { | ||
| localStorage.removeItem("VencordSettings"); | ||
| localStorage.removeItem("Vencord_settingsDirty"); | ||
| indexedDB.deleteDatabase("VencordData"); | ||
| indexedDB.deleteDatabase("VencordThemes"); | ||
| window.location.reload(); | ||
| }, | ||
| args: [] | ||
| }); | ||
| } | ||
| }); | ||
| document | ||
| .querySelector("#export-settings") | ||
| .addEventListener("click", async () => { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: async () => { | ||
| function getVencordQuickCss() { | ||
| return new Promise((resolve, reject) => { | ||
| const req = indexedDB.open("VencordData"); | ||
|
|
||
| req.onerror = () => reject(req.error); | ||
|
|
||
| req.onsuccess = () => { | ||
| const db = req.result; | ||
| const tx = db.transaction( | ||
| "VencordStore", | ||
| "readonly" | ||
| ); | ||
| const store = tx.objectStore("VencordStore"); | ||
| const getReq = store.get("VencordQuickCss"); | ||
|
|
||
| getReq.onsuccess = () => resolve(getReq.result); | ||
| getReq.onerror = () => reject(getReq.error); | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| const content = localStorage.getItem("VencordSettings"); | ||
| let quickCss = ""; | ||
| try { | ||
| quickCss = await getVencordQuickCss(); | ||
| } catch {} | ||
|
|
||
| if (content) { | ||
| const filename = `vencord-settings-backup-${ | ||
| new Date().toISOString().split("T")[0] | ||
| }.json`; | ||
|
|
||
| const a = document.createElement("a"); | ||
| a.href = URL.createObjectURL( | ||
| new File( | ||
| [ | ||
| new TextEncoder().encode( | ||
| JSON.stringify( | ||
| { | ||
| settings: JSON.parse(content), | ||
| quickCss: quickCss || "" | ||
| }, | ||
| null, | ||
| 4 | ||
| ) | ||
| ) | ||
| ], | ||
| filename, | ||
| { type: "application/json" } | ||
| ) | ||
| ); | ||
| a.download = filename; | ||
|
|
||
| document.body.appendChild(a); | ||
| a.click(); | ||
| setImmediate(() => { | ||
| URL.revokeObjectURL(a.href); | ||
| document.body.removeChild(a); | ||
| }); | ||
| } | ||
| }, | ||
| args: [] | ||
| }); | ||
| }); | ||
| document | ||
| .querySelector("#import-settings") | ||
| .addEventListener("click", async () => { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: async () => { | ||
| const input = document.createElement("input"); | ||
| input.type = "file"; | ||
| input.accept = ".json"; | ||
| input.onchange = async (event) => { | ||
| const file = event.target.files[0]; | ||
| if (!file) return; | ||
|
|
||
| const reader = new FileReader(); | ||
| reader.onload = async (e) => { | ||
| const content = e.target.result; | ||
| try { | ||
| const data = JSON.parse(content); | ||
| localStorage.setItem( | ||
| "VencordSettings", | ||
| JSON.stringify(data.settings) | ||
| ); | ||
| localStorage.setItem( | ||
| "Vencord_settingsDirty", | ||
| "true" | ||
| ); | ||
|
|
||
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const req = indexedDB.open( | ||
| "VencordData", | ||
| 1 | ||
| ); | ||
| req.onupgradeneeded = () => { | ||
| const db = req.result; | ||
| if ( | ||
| !db.objectStoreNames.contains( | ||
| "VencordStore" | ||
| ) | ||
| ) { | ||
| db.createObjectStore( | ||
| "VencordStore" | ||
| ); | ||
| } | ||
| }; | ||
| req.onerror = () => reject(req.error); | ||
| req.onsuccess = () => { | ||
| const db = req.result; | ||
| const tx = db.transaction( | ||
| "VencordStore", | ||
| "readwrite" | ||
| ); | ||
| const store = | ||
| tx.objectStore("VencordStore"); | ||
| const putReq = store.put( | ||
| data.quickCss || "", | ||
| "VencordQuickCss" | ||
| ); | ||
| putReq.onsuccess = () => resolve(); | ||
| putReq.onerror = () => | ||
| reject(putReq.error); | ||
| tx.oncomplete = () => db.close(); | ||
| tx.onerror = () => reject(tx.error); | ||
| }; | ||
| }); | ||
| } catch {} | ||
|
|
||
| window.location.reload(); | ||
| } catch (error) { | ||
| alert( | ||
| "Failed to import settings. Is this even a Vencord Settings file?" | ||
| ); | ||
| } | ||
| }; | ||
| reader.readAsText(file); | ||
| }; | ||
| input.click(); | ||
| }, | ||
| args: [] | ||
| }); | ||
| }); |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent API usage: All instances of chrome.scripting.executeScript and chrome.tabs.query should use the api constant defined at the top of the file for cross-browser compatibility (supporting both Chrome and Firefox).
| args: [] | ||
| }); | ||
| } | ||
| }); | ||
| document | ||
| .querySelector("#disable-plugins") | ||
| .addEventListener("click", async () => { | ||
| if ( | ||
| confirm( | ||
| "This will disable all plugins and reload Discord. Do you want to continue?" | ||
| ) | ||
| ) { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: () => { | ||
| const rawSettings = | ||
| localStorage.getItem("VencordSettings"); | ||
| if (!rawSettings) return; | ||
| const settings = JSON.parse(rawSettings); | ||
| for (const name in settings.plugins) { | ||
| settings.plugins[name].enabled = false; | ||
| } | ||
| localStorage.setItem( | ||
| "VencordSettings", | ||
| JSON.stringify(settings) | ||
| ); | ||
| localStorage.setItem("Vencord_settingsDirty", "true"); | ||
|
|
||
| window.location.reload(); | ||
| }, | ||
| args: [] | ||
| }); | ||
| } | ||
| }); | ||
| document.querySelector("#reset").addEventListener("click", async () => { | ||
| if ( | ||
| prompt( | ||
| 'This will clear all your Vencord settings in this browser, including plugins, themes and QuickCSS! Make sure that you have a backup before continuing. To continue, type "YES".' | ||
| ) === "YES" | ||
| ) { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: () => { | ||
| localStorage.removeItem("VencordSettings"); | ||
| localStorage.removeItem("Vencord_settingsDirty"); | ||
| indexedDB.deleteDatabase("VencordData"); | ||
| indexedDB.deleteDatabase("VencordThemes"); | ||
| window.location.reload(); | ||
| }, | ||
| args: [] | ||
| }); | ||
| } | ||
| }); | ||
| document | ||
| .querySelector("#export-settings") | ||
| .addEventListener("click", async () => { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: async () => { | ||
| function getVencordQuickCss() { | ||
| return new Promise((resolve, reject) => { | ||
| const req = indexedDB.open("VencordData"); | ||
|
|
||
| req.onerror = () => reject(req.error); | ||
|
|
||
| req.onsuccess = () => { | ||
| const db = req.result; | ||
| const tx = db.transaction( | ||
| "VencordStore", | ||
| "readonly" | ||
| ); | ||
| const store = tx.objectStore("VencordStore"); | ||
| const getReq = store.get("VencordQuickCss"); | ||
|
|
||
| getReq.onsuccess = () => resolve(getReq.result); | ||
| getReq.onerror = () => reject(getReq.error); | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| const content = localStorage.getItem("VencordSettings"); | ||
| let quickCss = ""; | ||
| try { | ||
| quickCss = await getVencordQuickCss(); | ||
| } catch {} | ||
|
|
||
| if (content) { | ||
| const filename = `vencord-settings-backup-${ | ||
| new Date().toISOString().split("T")[0] | ||
| }.json`; | ||
|
|
||
| const a = document.createElement("a"); | ||
| a.href = URL.createObjectURL( | ||
| new File( | ||
| [ | ||
| new TextEncoder().encode( | ||
| JSON.stringify( | ||
| { | ||
| settings: JSON.parse(content), | ||
| quickCss: quickCss || "" | ||
| }, | ||
| null, | ||
| 4 | ||
| ) | ||
| ) | ||
| ], | ||
| filename, | ||
| { type: "application/json" } | ||
| ) | ||
| ); | ||
| a.download = filename; | ||
|
|
||
| document.body.appendChild(a); | ||
| a.click(); | ||
| setImmediate(() => { | ||
| URL.revokeObjectURL(a.href); | ||
| document.body.removeChild(a); | ||
| }); | ||
| } | ||
| }, | ||
| args: [] | ||
| }); | ||
| }); | ||
| document | ||
| .querySelector("#import-settings") | ||
| .addEventListener("click", async () => { | ||
| const tab = await getDiscordTab(); | ||
| await chrome.scripting.executeScript({ | ||
| target: { | ||
| tabId: tab.id | ||
| }, | ||
| func: async () => { | ||
| const input = document.createElement("input"); | ||
| input.type = "file"; | ||
| input.accept = ".json"; | ||
| input.onchange = async (event) => { | ||
| const file = event.target.files[0]; | ||
| if (!file) return; | ||
|
|
||
| const reader = new FileReader(); | ||
| reader.onload = async (e) => { | ||
| const content = e.target.result; | ||
| try { | ||
| const data = JSON.parse(content); | ||
| localStorage.setItem( | ||
| "VencordSettings", | ||
| JSON.stringify(data.settings) | ||
| ); | ||
| localStorage.setItem( | ||
| "Vencord_settingsDirty", | ||
| "true" | ||
| ); | ||
|
|
||
| try { | ||
| await new Promise((resolve, reject) => { | ||
| const req = indexedDB.open( | ||
| "VencordData", | ||
| 1 | ||
| ); | ||
| req.onupgradeneeded = () => { | ||
| const db = req.result; | ||
| if ( | ||
| !db.objectStoreNames.contains( | ||
| "VencordStore" | ||
| ) | ||
| ) { | ||
| db.createObjectStore( | ||
| "VencordStore" | ||
| ); | ||
| } | ||
| }; | ||
| req.onerror = () => reject(req.error); | ||
| req.onsuccess = () => { | ||
| const db = req.result; | ||
| const tx = db.transaction( | ||
| "VencordStore", | ||
| "readwrite" | ||
| ); | ||
| const store = | ||
| tx.objectStore("VencordStore"); | ||
| const putReq = store.put( | ||
| data.quickCss || "", | ||
| "VencordQuickCss" | ||
| ); | ||
| putReq.onsuccess = () => resolve(); | ||
| putReq.onerror = () => | ||
| reject(putReq.error); | ||
| tx.oncomplete = () => db.close(); | ||
| tx.onerror = () => reject(tx.error); | ||
| }; | ||
| }); | ||
| } catch {} | ||
|
|
||
| window.location.reload(); | ||
| } catch (error) { | ||
| alert( | ||
| "Failed to import settings. Is this even a Vencord Settings file?" | ||
| ); | ||
| } | ||
| }; | ||
| reader.readAsText(file); | ||
| }; | ||
| input.click(); | ||
| }, | ||
| args: [] | ||
| }); |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused args parameter: The args: [] parameter is consistently passed but never used in any of the injected functions. Consider removing it for cleaner code.
| ], | ||
|
|
||
| "permissions": ["declarativeNetRequest"], | ||
| "permissions": ["scripting", "declarativeNetRequest"], |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "scripting" permission is added to support the popup functionality (executeScript API), but the "activeTab" permission would be more appropriate and privacy-friendly. The "activeTab" permission only grants temporary access to the current tab when the user invokes the extension (e.g., clicking the popup), rather than requiring broad scripting permissions across all tabs.
| "permissions": ["scripting", "declarativeNetRequest"], | |
| "permissions": ["activeTab", "declarativeNetRequest"], |
| <p>Vencord</p> | ||
| <p id="local-version">v</p> |
Copilot
AI
Dec 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Semantic HTML issue: Using <p> tags within an <h1> for the version and commit hash is semantically incorrect. The <p> element represents a paragraph and shouldn't be nested inside a heading. Consider using <span> elements instead, which are appropriate for inline text within headings.
| <p>Vencord</p> | |
| <p id="local-version">v</p> | |
| <span>Vencord</span> | |
| <span id="local-version">v</span> |
|
thanks for training copilot on some of my work vee |
|
ywyw |

Uh oh!
There was an error while loading. Please reload this page.