34 Commits

Author SHA1 Message Date
V
49fb4c68b6 Updater Popup: Add Changelog; Make about page prettier 2023-06-26 01:42:51 +02:00
V
477ecbb4ba Cleanup 2023-06-26 00:41:52 +02:00
V
4abce9d084 Fix some Context Menus being broken by regression 2023-06-26 00:05:08 +02:00
V
7f54858b27 Add SpellCheck toggle in textarea context menu 2023-06-25 16:48:46 +02:00
V
be176fab71 Update README.md (#42) 2023-06-25 04:55:06 +02:00
V
e60f04bb79 Fix DevTools context menu 2023-06-25 04:20:26 +02:00
V
17f1c4ff6f Bump to v0.2.4 2023-06-25 03:45:44 +02:00
V
a7ded71404 Add SpellCheck suggestions 2023-06-25 03:44:19 +02:00
V
31799ccfb0 Add Notification badge 2023-06-23 17:20:54 +02:00
V
dde696627e Bump to v0.2.3 2023-06-23 16:04:09 +02:00
V
50b2e864c2 Update UserAgents 2023-06-23 16:03:31 +02:00
V
dfa007669b arrpc: Install from GitHub instead of npm 2023-06-23 16:01:50 +02:00
V
4b27c67e83 Bump electron from v23 to v25 2023-06-23 15:56:26 +02:00
V
f58ed485a9 Improve defaults, Add Rich Presence to first launch confi 2023-06-22 15:55:04 +02:00
V
c31eb8154b Bump to v0.2.2 2023-06-21 20:53:28 +02:00
V
253277984b Add ScreenSharing (#14) 2023-06-21 20:52:56 +02:00
V
23c0647e6c Fix ENOENT on first install 2023-06-21 17:24:50 +02:00
V
fd45068a46 autoStart: respect XDG_CONFIG_HOME 2023-06-21 17:09:04 +02:00
V
9f9f665ede blehhhhh 2023-06-21 16:53:04 +02:00
V
fd0055032f Add AutoStart to first launch tour 2023-06-21 16:52:28 +02:00
V
a993d34c9d autostart: improve typing 2023-06-21 16:15:57 +02:00
V
f232defd1c Add start with system option 2023-06-21 16:13:20 +02:00
V
887f11ab37 About page: Add Acknowledgements 2023-06-21 15:06:06 +02:00
V
4c34f10bb2 bump to v.0.2.1 2023-06-21 14:47:25 +02:00
V
684f3330e9 Fix updater file logic 2023-06-21 14:46:40 +02:00
V
178bb0c012 Add first launch tour (#38) 2023-06-21 14:39:41 +02:00
V
39cc30de53 Add arRPC integration 2023-06-09 22:47:59 +02:00
Priultimus
aae4223294 Add MacOs arm64 build (#37) 2023-05-24 03:02:30 +02:00
V
bddfa60f19 Update README.md 2023-05-05 01:30:11 +02:00
V
58b10cfcfe Update README.md 2023-05-05 01:29:24 +02:00
Vendicated
36a751bb7b v0.2.0 2023-05-03 04:14:03 +02:00
Vendicated
6a67f07a9e Add portable target 2023-05-03 04:13:43 +02:00
V
c43871950c Update README.md 2023-04-28 02:38:59 +02:00
Vendicated
fe70701de8 Fix zoom & about menu items; add version to about page 2023-04-27 02:23:17 +02:00
53 changed files with 1381 additions and 182 deletions

View File

@@ -9,6 +9,12 @@
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

View File

@@ -1,8 +1,13 @@
# Vencord Desktop
A standalone Electron app that loads Discord & Vencord
Vencord Desktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed
Vencord Desktop is currently in very early alpha. Bug reports, feature requests & contributions are highly appreciated!!
**Not yet supported**:
- Global Keybinds
Bug reports, feature requests & contributions are highly appreciated!!
![image](https://user-images.githubusercontent.com/45497981/235024615-94565eaf-f412-4384-a3f5-d8cde7458f6d.png)
## Installing

View File

@@ -1,6 +1,6 @@
{
"name": "VencordDesktop",
"version": "0.1.9",
"version": "0.2.5",
"private": true,
"description": "",
"keywords": [],
@@ -22,6 +22,9 @@
"testTypes": "tsc --noEmit",
"watch": "pnpm build --watch"
},
"dependencies": {
"arrpc": "github:OpenAsar/arrpc#3eb5d36a5e9295d3aeafc49975df5d399eb627fd"
},
"devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/node": "^18.15.11",
@@ -30,7 +33,7 @@
"@typescript-eslint/parser": "^5.57.1",
"@vencord/types": "^0.1.2",
"dotenv": "^16.0.3",
"electron": "^23.2.0",
"electron": "^25.2.0",
"electron-builder": "^23.6.0",
"esbuild": "^0.17.14",
"eslint": "^8.38.0",
@@ -80,11 +83,30 @@
}
},
"mac": {
"target": [
{
"target": "default",
"arch": [
"x64",
"arm64"
]
}
],
"category": "Network"
},
"nsis": {
"include": "build/installer.nsh",
"oneClick": false
},
"win": {
"target": [
"nsis",
"portable"
]
},
"publish": {
"provider": "github",
"releaseType": "release"
}
}
}

43
pnpm-lock.yaml generated
View File

@@ -1,5 +1,10 @@
lockfileVersion: '6.0'
dependencies:
arrpc:
specifier: github:OpenAsar/arrpc#3eb5d36a5e9295d3aeafc49975df5d399eb627fd
version: github.com/OpenAsar/arrpc/3eb5d36a5e9295d3aeafc49975df5d399eb627fd
devDependencies:
'@fal-works/esbuild-plugin-global-externals':
specifier: ^2.1.2
@@ -23,8 +28,8 @@ devDependencies:
specifier: ^16.0.3
version: 16.0.3
electron:
specifier: ^23.2.0
version: 23.2.0
specifier: ^25.2.0
version: 25.2.0
electron-builder:
specifier: ^23.6.0
version: 23.6.0
@@ -515,10 +520,6 @@ packages:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true
/@types/node@16.18.22:
resolution: {integrity: sha512-LJSIirgASa1LicFGTUFwDY7BfKDtLIbijqDLkH47LxEo/jtdrtiZ4/kLPD99bEQhTcPcuh6KhDllHqRxygJD2w==}
dev: true
/@types/node@18.15.11:
resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==}
dev: true
@@ -1508,14 +1509,14 @@ packages:
- supports-color
dev: true
/electron@23.2.0:
resolution: {integrity: sha512-De9e21cri0QYct/w6tTNOnKyCt9RVKUw5F8PEN4FPzGR9tr6IT53uyt42uH754uJWrZeLMCAdoXy6/0GmMmYZA==}
/electron@25.2.0:
resolution: {integrity: sha512-I/rhcW2sV2fyiveVSBr2N7v5ZiCtdGY0UiNCDZgk2fpSC+irQjbeh7JT2b4vWmJ2ogOXBjqesrN9XszTIG6DHg==}
engines: {node: '>= 12.20.55'}
hasBin: true
requiresBuild: true
dependencies:
'@electron/get': 2.0.2
'@types/node': 16.18.22
'@types/node': 18.15.11
extract-zip: 2.0.1
transitivePeerDependencies:
- supports-color
@@ -3785,6 +3786,19 @@ packages:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
/ws@8.13.0:
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/xmlbuilder@15.1.1:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
@@ -3829,3 +3843,14 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
github.com/OpenAsar/arrpc/3eb5d36a5e9295d3aeafc49975df5d399eb627fd:
resolution: {tarball: https://codeload.github.com/OpenAsar/arrpc/tar.gz/3eb5d36a5e9295d3aeafc49975df5d399eb627fd}
name: arrpc
version: 3.1.0
dependencies:
ws: 8.13.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false

2
src/globals.d.ts vendored
View File

@@ -7,7 +7,7 @@
declare global {
export var VencordDesktopNative: typeof import("preload/VencordDesktopNative").VencordDesktopNative;
export var VencordDesktop: typeof import("renderer/index");
export var vcdLS: typeof localStorage;
export var VCDP: any;
export var IS_DEV: boolean;
}

View File

@@ -6,7 +6,7 @@
import { BrowserWindow } from "electron";
import { join } from "path";
import { ICON_PATH, STATIC_DIR } from "shared/paths";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
@@ -14,12 +14,15 @@ export function createAboutWindow() {
const about = new BrowserWindow({
center: true,
autoHideMenuBar: true,
icon: ICON_PATH
icon: ICON_PATH,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
}
});
makeLinksOpenExternally(about);
about.loadFile(join(STATIC_DIR, "about.html"));
about.loadFile(join(VIEW_DIR, "about.html"));
return about;
}

50
src/main/appBadge.ts Normal file
View File

@@ -0,0 +1,50 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { app, NativeImage, nativeImage } from "electron";
import { join } from "path";
import { BADGE_DIR } from "shared/paths";
const imgCache = new Map<number, NativeImage>();
function loadBadge(index: number) {
const cached = imgCache.get(index);
if (cached) return cached;
const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.ico`));
imgCache.set(index, img);
return img;
}
let lastIndex: null | number = -1;
export function setBadgeCount(count: number) {
switch (process.platform) {
case "darwin":
case "linux":
if (count === -1) count = 0;
app.setBadgeCount(count);
break;
case "win32":
const [index, description] = getBadgeIndexAndDescription(count);
if (lastIndex === index) break;
lastIndex = index;
// circular import shenanigans
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
break;
}
}
function getBadgeIndexAndDescription(count: number): [number | null, string] {
if (count === -1) return [11, "Unread Messages"];
if (count === 0) return [null, "No Notifications"];
const index = Math.max(1, Math.min(count, 10));
return [index, `${index} Notification`];
}

21
src/main/arrpc.ts Normal file
View File

@@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import Server from "arrpc";
import { send as sendToBridge } from "arrpc/src/bridge";
import { Settings } from "./settings";
let server: any;
export async function initArRPC() {
if (server || !Settings.store.arRPC) return;
server = await new Server();
server.on("activity", sendToBridge);
}
Settings.addChangeListener("arRPC", initArRPC);

49
src/main/autoStart.ts Normal file
View File

@@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { app } from "electron";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
import { join } from "path";
interface AutoStart {
isEnabled(): boolean;
enable(): void;
disable(): void;
}
function makeAutoStartLinux(): AutoStart {
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
const dir = join(configDir, "autostart");
const file = join(dir, "vencord.desktop");
return {
isEnabled: () => existsSync(file),
enable() {
const desktopFile = `
[Desktop Entry]
Type=Application
Version=1.0
Name=Vencord
Comment=Vencord autostart script
Exec=${process.execPath}
Terminal=false
StartupNotify=false
`.trim();
mkdirSync(dir, { recursive: true });
writeFileSync(file, desktopFile);
},
disable: () => rmSync(file, { force: true })
};
}
const autoStartWindowsMac: AutoStart = {
isEnabled: () => app.getLoginItemSettings().openAtLogin,
enable: () => app.setLoginItemSettings({ openAtLogin: true }),
disable: () => app.setLoginItemSettings({ openAtLogin: false })
};
export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac;

View File

@@ -17,7 +17,7 @@ export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json")
export const VENCORD_FILES_DIR =
(require("./settings") as typeof import("./settings")).Settings.store.vencordDir || join(DATA_DIR, "vencordDist");
export const USER_AGENT = `VencordDesktop/${app.getVersion()} (https://github.com/Vencord/Electron)`;
export const USER_AGENT = `VencordDesktop/${app.getVersion()} (https://github.com/Vencord/Desktop)`;
// dimensions shamelessly stolen from Discord Desktop :3
export const MIN_WIDTH = 940;

72
src/main/firstLaunch.ts Normal file
View File

@@ -0,0 +1,72 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { app } from "electron";
import { BrowserWindow } from "electron/main";
import { copyFileSync, mkdirSync, readdirSync } from "fs";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { VIEW_DIR } from "shared/paths";
import { autoStart } from "./autoStart";
import { DATA_DIR } from "./constants";
import { createWindows } from "./mainWindow";
import { Settings } from "./settings";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
interface Data {
minimizeToTray: boolean;
discordBranch: "stable" | "canary" | "ptb";
autoStart: boolean;
importSettings: boolean;
richPresence: boolean;
}
export function createFirstLaunchTour() {
const win = new BrowserWindow({
...SplashProps,
frame: true,
autoHideMenuBar: true,
height: 470,
width: 550
});
makeLinksOpenExternally(win);
win.loadFile(join(VIEW_DIR, "first-launch.html"));
win.webContents.addListener("console-message", (_e, _l, msg) => {
if (msg === "cancel") return app.exit();
if (!msg.startsWith("form:")) return;
const data = JSON.parse(msg.slice(5)) as Data;
Settings.store.minimizeToTray = data.minimizeToTray;
Settings.store.discordBranch = data.discordBranch;
Settings.store.firstLaunch = false;
Settings.store.arRPC = data.richPresence;
if (data.autoStart) autoStart.enable();
if (data.importSettings) {
const from = join(app.getPath("userData"), "..", "Vencord", "settings");
const to = join(DATA_DIR, "settings");
try {
const files = readdirSync(from);
mkdirSync(to, { recursive: true });
for (const file of files) {
copyFileSync(join(from, file), join(to, file));
}
} catch (e) {
console.error("Failed to import settings:", e);
}
}
win.close();
createWindows();
});
}

View File

@@ -7,16 +7,14 @@
import "./ipc";
import { app, BrowserWindow } from "electron";
import { join } from "path";
import { checkUpdates } from "updater/main";
import { ICON_PATH } from "../shared/paths";
import { once } from "../shared/utils/once";
import { DATA_DIR, VENCORD_FILES_DIR } from "./constants";
import { createMainWindow } from "./mainWindow";
import { DATA_DIR } from "./constants";
import { createFirstLaunchTour } from "./firstLaunch";
import { createWindows, mainWin } from "./mainWindow";
import { registerScreenShareHandler } from "./screenShare";
import { Settings } from "./settings";
import { createSplashWindow } from "./splash";
import { ensureVencordFiles } from "./utils/vencordLoader";
if (IS_DEV) {
require("source-map-support").install();
@@ -25,25 +23,19 @@ if (IS_DEV) {
// Make the Vencord files use our DATA_DIR
process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
let mainWin: BrowserWindow | null = null;
function init() {
// <-- BEGIN COPY PASTED FROM DISCORD -->
// work around chrome 66 disabling autoplay by default
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
// WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
// HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
//
// WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
app.commandLine.appendSwitch(
"disable-features",
"WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService"
"WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering"
);
// <-- END COPY PASTED FROM DISCORD -->
app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => {
if (data.IS_DEV) app.quit();
else if (mainWin) {
@@ -58,7 +50,8 @@ function init() {
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
else if (process.platform === "darwin") app.dock.setIcon(ICON_PATH);
createWindows();
registerScreenShareHandler();
bootstrap();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindows();
@@ -78,22 +71,12 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
init();
}
async function createWindows() {
const splash = createSplashWindow();
await ensureVencordFiles();
runVencordMain();
mainWin = createMainWindow();
mainWin.once("ready-to-show", () => {
splash.destroy();
mainWin!.show();
if (Settings.store.maximized) {
mainWin!.maximize();
}
});
async function bootstrap() {
if (!Object.hasOwn(Settings.store, "firstLaunch")) {
createFirstLaunchTour();
} else {
createWindows();
}
}
app.on("window-all-closed", () => {

View File

@@ -11,6 +11,8 @@ import { join } from "path";
import { debounce } from "shared/utils/debounce";
import { IpcEvents } from "../shared/IpcEvents";
import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart";
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE } from "./constants";
import { mainWin } from "./mainWindow";
import { Settings } from "./settings";
@@ -39,6 +41,12 @@ ipcMain.on(IpcEvents.GET_VERSION, e => {
e.returnValue = app.getVersion();
});
ipcMain.on(IpcEvents.AUTOSTART_ENABLED, e => {
e.returnValue = autoStart.isEnabled();
});
ipcMain.handle(IpcEvents.ENABLE_AUTOSTART, autoStart.enable);
ipcMain.handle(IpcEvents.DISABLE_AUTOSTART, autoStart.disable);
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: string) => {
Settings.setData(settings, path);
});
@@ -68,6 +76,14 @@ ipcMain.handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => {
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
});
ipcMain.handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
e.sender.replaceMisspelling(word);
});
ipcMain.handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
e.sender.session.addWordToSpellCheckerDictionary(word);
});
ipcMain.handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
const res = await dialog.showOpenDialog(mainWin!, {
properties: ["openDirectory"]
@@ -82,6 +98,8 @@ ipcMain.handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
return dir;
});
ipcMain.handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
function readCss() {
return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
}

View File

@@ -6,13 +6,18 @@
import { app, BrowserWindow, BrowserWindowConstructorOptions, Menu, Tray } from "electron";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { once } from "shared/utils/once";
import type { SettingsStore } from "shared/utils/SettingsStore";
import { ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about";
import { DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH } from "./constants";
import { initArRPC } from "./arrpc";
import { DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH, VENCORD_FILES_DIR } from "./constants";
import { Settings, VencordSettings } from "./settings";
import { createSplashWindow } from "./splash";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { downloadVencordFiles } from "./utils/vencordLoader";
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
let isQuitting = false;
let tray: Tray;
@@ -23,6 +28,27 @@ app.on("before-quit", () => {
export let mainWin: BrowserWindow;
function makeSettingsListenerHelpers<O extends object>(o: SettingsStore<O>) {
const listeners = new Map<(data: any) => void, PropertyKey>();
const addListener: typeof o.addChangeListener = (path, cb) => {
listeners.set(cb, path);
o.addChangeListener(path, cb);
};
const removeAllListeners = () => {
for (const [listener, path] of listeners) {
o.removeChangeListener(path as any, listener);
}
listeners.clear();
};
return [addListener, removeAllListeners] as const;
}
const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings);
const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
function initTray(win: BrowserWindow) {
const trayMenu = Menu.buildFromTemplate([
{
@@ -88,7 +114,6 @@ function initMenuBar(win: BrowserWindow) {
submenu: [
{
label: "About Vencord Desktop",
role: "about",
click: createAboutWindow
},
{
@@ -125,25 +150,20 @@ function initMenuBar(win: BrowserWindow) {
click() {
app.quit();
}
},
// See https://github.com/electron/electron/issues/14742 and https://github.com/electron/electron/issues/5256
{
label: "Zoom in (hidden, hack for Qwertz and others)",
accelerator: "CmdOrCtrl+=",
role: "zoomIn",
visible: false
}
]
},
{ role: "fileMenu" },
{ role: "editMenu" },
{ role: "viewMenu" },
{ role: "windowMenu" },
{
label: "Zoom",
submenu: [
// See https://github.com/electron/electron/issues/14742 and https://github.com/electron/electron/issues/5256
{
label: "Zoom in",
accelerator: "CmdOrCtrl+=",
role: "zoomIn"
}
],
visible: false
}
{ role: "windowMenu" }
]);
Menu.setApplicationMenu(menu);
@@ -189,11 +209,11 @@ function initWindowBoundsListeners(win: BrowserWindow) {
}
function initSettingsListeners(win: BrowserWindow) {
Settings.addChangeListener("tray", enable => {
addSettingsListener("tray", enable => {
if (enable) initTray(win);
else tray?.destroy();
});
Settings.addChangeListener("disableMinSize", disable => {
addSettingsListener("disableMinSize", disable => {
if (disable) {
// 0 no work
win.setMinimumSize(1, 1);
@@ -208,7 +228,7 @@ function initSettingsListeners(win: BrowserWindow) {
}
});
VencordSettings.addChangeListener("macosTranslucency", enabled => {
addVencordSettingsListener("macosTranslucency", enabled => {
if (enabled) {
win.setVibrancy("sidebar");
win.setBackgroundColor("#ffffff00");
@@ -219,7 +239,17 @@ function initSettingsListeners(win: BrowserWindow) {
});
}
export function createMainWindow() {
function initSpellCheck(win: BrowserWindow) {
win.webContents.on("context-menu", (_, data) => {
win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
});
}
function createMainWindow() {
// Clear up previous settings listeners
removeSettingsListeners();
removeVencordSettingsListeners();
const win = (mainWin = new BrowserWindow({
show: false,
autoHideMenuBar: true,
@@ -228,7 +258,8 @@ export function createMainWindow() {
sandbox: false,
contextIsolation: true,
devTools: true,
preload: join(__dirname, "preload.js")
preload: join(__dirname, "preload.js"),
spellcheck: true
},
icon: ICON_PATH,
frame: VencordSettings.store.frameless !== true,
@@ -258,9 +289,10 @@ export function createMainWindow() {
initMenuBar(win);
makeLinksOpenExternally(win);
initSettingsListeners(win);
initSpellCheck(win);
win.webContents.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
);
const subdomain =
@@ -272,3 +304,25 @@ export function createMainWindow() {
return win;
}
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
export async function createWindows() {
const splash = createSplashWindow();
await ensureVencordFiles();
runVencordMain();
mainWin = createMainWindow();
mainWin.once("ready-to-show", () => {
splash.destroy();
mainWin!.show();
if (Settings.store.maximized) {
mainWin!.maximize();
}
});
initArRPC();
}

55
src/main/screenShare.ts Normal file
View File

@@ -0,0 +1,55 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { desktopCapturer, ipcMain, session, Streams } from "electron";
import type { StreamPick } from "renderer/components/ScreenSharePicker";
import { IpcEvents } from "shared/IpcEvents";
export function registerScreenShareHandler() {
ipcMain.handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => {
const sources = await desktopCapturer.getSources({
types: ["window", "screen"],
thumbnailSize: {
width: 1920,
height: 1080
}
});
return sources.find(s => s.id === id)?.thumbnail.toDataURL();
});
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
const sources = await desktopCapturer.getSources({
types: ["window", "screen"],
thumbnailSize: {
width: 176,
height: 99
}
});
const data = sources.map(({ id, name, thumbnail }) => ({
id,
name,
url: thumbnail.toDataURL()
}));
const choice = await request.frame
.executeJavaScript(`VencordDesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`)
.then(e => e as StreamPick)
.catch(() => null);
if (!choice) return callback({});
const source = sources.find(s => s.id === choice.id);
if (!source) return callback({});
const streams: Streams = {
video: source
};
if (choice.audio && process.platform === "win32") streams.audio = "loopback";
callback(streams);
});
}

View File

@@ -4,8 +4,8 @@
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import type { Settings as TSettings } from "shared/settings";
import { SettingsStore } from "shared/utils/SettingsStore";
@@ -25,7 +25,10 @@ function loadSettings<T extends object = any>(file: string, name: string) {
} catch {}
const store = new SettingsStore(settings);
store.addGlobalChangeListener(o => writeFileSync(file, JSON.stringify(o, null, 4)));
store.addGlobalChangeListener(o => {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, JSON.stringify(o, null, 4));
});
return store;
}

View File

@@ -7,12 +7,12 @@
import { BrowserWindow } from "electron";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { STATIC_DIR } from "shared/paths";
import { VIEW_DIR } from "shared/paths";
export function createSplashWindow() {
const splash = new BrowserWindow(SplashProps);
splash.loadFile(join(STATIC_DIR, "splash.html"));
splash.loadFile(join(VIEW_DIR, "splash.html"));
return splash;
}

View File

@@ -4,16 +4,31 @@
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { ipcRenderer } from "electron";
import type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest";
import { IpcEvents } from "../shared/IpcEvents";
import { invoke, sendSync } from "./typedIpcs";
import { invoke, sendSync } from "./typedIpc";
type SpellCheckerResultCallback = (word: string, suggestions: string[]) => void;
const spellCheckCallbacks = new Set<SpellCheckerResultCallback>();
ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => {
spellCheckCallbacks.forEach(cb => cb(w, s));
});
export const VencordDesktopNative = {
app: {
relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION)
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count)
},
autostart: {
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
enable: () => invoke<void>(IpcEvents.ENABLE_AUTOSTART),
disable: () => invoke<void>(IpcEvents.DISABLE_AUTOSTART)
},
fileManager: {
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
@@ -24,10 +39,20 @@ export const VencordDesktopNative = {
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
},
spellcheck: {
setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages)
// todo: perhaps add ways to learn words
setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages),
onSpellcheckResult(cb: SpellCheckerResultCallback) {
spellCheckCallbacks.add(cb);
},
offSpellcheckResult(cb: SpellCheckerResultCallback) {
spellCheckCallbacks.delete(cb);
},
replaceMisspelling: (word: string) => invoke<void>(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, word),
addToDictionary: (word: string) => invoke<void>(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word)
},
win: {
focus: () => invoke<void>(IpcEvents.FOCUS)
},
capturer: {
getLargeThumbnail: (id: string) => invoke<string>(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id)
}
};

43
src/renderer/appBadge.ts Normal file
View File

@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { filters, waitFor } from "@vencord/types/webpack";
import { RelationshipStore } from "@vencord/types/webpack/common";
import { Settings } from "./settings";
let GuildReadStateStore: any;
let NotificationSettingsStore: any;
export function setBadge() {
if (Settings.store.appBadge === false) return;
const mentionCount = GuildReadStateStore.getTotalMentionCount();
const pendingRequests = RelationshipStore.getPendingCount();
const hasUnread = GuildReadStateStore.hasAnyUnread();
const disableUnreadBadge = NotificationSettingsStore.getDisableUnreadBadge();
let totalCount = mentionCount + pendingRequests;
if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1;
VencordDesktopNative.app.setBadgeCount(totalCount);
}
let toFind = 3;
function waitForAndSubscribeToStore(name: string, cb?: (m: any) => void) {
waitFor(filters.byStoreName(name), store => {
cb?.(store);
store.addChangeListener(setBadge);
toFind--;
if (toFind === 0) setBadge();
});
}
waitForAndSubscribeToStore("GuildReadStateStore", store => (GuildReadStateStore = store));
waitForAndSubscribeToStore("NotificationSettingsStore", store => (NotificationSettingsStore = store));
waitForAndSubscribeToStore("RelationshipStore");

View File

@@ -0,0 +1,225 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import "./screenSharePicker.css";
import { classes, closeModal, Margins, Modals, openModal, useAwaiter } from "@vencord/types/utils";
import { findByPropsLazy } from "@vencord/types/webpack";
import { Button, Card, Forms, Switch, Text, useState } from "@vencord/types/webpack/common";
import type { Dispatch, SetStateAction } from "react";
import { isWindows } from "renderer/utils";
const StreamResolutions = ["720", "1080", "1440", "Source"] as const;
const StreamFps = ["15", "30", "60"] as const;
const WarningIconClasses = findByPropsLazy("warning", "error", "container");
export type StreamResolution = (typeof StreamResolutions)[number];
export type StreamFps = (typeof StreamFps)[number];
interface StreamSettings {
resolution: StreamResolution;
fps: StreamFps;
audio: boolean;
}
export interface StreamPick extends StreamSettings {
id: string;
}
interface Source {
id: string;
name: string;
url: string;
}
export function openScreenSharePicker(screens: Source[]) {
return new Promise<StreamPick>((resolve, reject) => {
const key = openModal(
props => (
<ModalComponent
screens={screens}
modalProps={props}
submit={resolve}
close={() => {
props.onClose();
reject("Aborted");
}}
/>
),
{
onCloseRequest() {
closeModal(key);
reject("Aborted");
}
}
);
});
}
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) {
return (
<div className="vcd-screen-picker-grid">
{screens.map(({ id, name, url }) => (
<label key={id}>
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
<img src={url} alt="" />
<Text variant="text-sm/normal">{name}</Text>
</label>
))}
</div>
);
}
function StreamSettings({
source,
settings,
setSettings
}: {
source: Source;
settings: StreamSettings;
setSettings: Dispatch<SetStateAction<StreamSettings>>;
}) {
const [thumb] = useAwaiter(() => VencordDesktopNative.capturer.getLargeThumbnail(source.id), {
fallbackValue: source.url,
deps: [source.id]
});
return (
<div>
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
<img src={thumb} alt="" />
<Text variant="text-sm/normal">{source.name}</Text>
</Card>
<Forms.FormTitle>Stream Settings</Forms.FormTitle>
<Card className="vcd-screen-picker-card">
<Card className={classes(WarningIconClasses.container, WarningIconClasses.warning, Margins.bottom8)}>
<Forms.FormText>
Resolution and Frame Rate aren't implemented for now. Locked to 720p 30fps
</Forms.FormText>
</Card>
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Resolution</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => (
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
<Text variant="text-sm/bold">{res}</Text>
<input
type="radio"
name="resolution"
value={res}
checked={settings.resolution === res}
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
/>
</label>
))}
</div>
</section>
<section>
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamFps.map(fps => (
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<Text variant="text-sm/bold">{fps}</Text>
<input
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/>
</label>
))}
</div>
</section>
</div>
{isWindows && (
<Switch
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder
className="vcd-screen-picker-audio"
>
Stream With Audio
</Switch>
)}
</Card>
</div>
);
}
function ModalComponent({
screens,
modalProps,
submit,
close
}: {
screens: Source[];
modalProps: any;
submit: (data: StreamPick) => void;
close: () => void;
}) {
const [selected, setSelected] = useState<string>();
const [settings, setSettings] = useState<StreamSettings>({
resolution: "1080",
fps: "60",
audio: true
});
return (
<Modals.ModalRoot {...modalProps}>
<Modals.ModalHeader className="vcd-screen-picker-header">
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} />
</Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal">
{!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} />
) : (
<StreamSettings
source={screens.find(s => s.id === selected)!}
settings={settings}
setSettings={setSettings}
/>
)}
</Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer">
<Button
disabled={!selected}
onClick={() => {
submit({
id: selected!,
...settings
});
close();
}}
>
Go Live
</Button>
{selected ? (
<Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
Back
</Button>
) : (
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
Cancel
</Button>
)}
</Modals.ModalFooter>
</Modals.ModalRoot>
);
}

View File

@@ -7,12 +7,16 @@
import "./settings.css";
import { Margins } from "@vencord/types/utils";
import { Button, Forms, Select, Switch, Text } from "@vencord/types/webpack/common";
import { Button, Forms, Select, Switch, Text, useState } from "@vencord/types/webpack/common";
import { setBadge } from "renderer/appBadge";
import { useSettings } from "renderer/settings";
export default function SettingsUi() {
const Settings = useSettings();
const { autostart } = VencordDesktopNative;
const [autoStartEnabled, setAutoStartEnabled] = useState(autostart.isEnabled());
const switches: [keyof typeof Settings, string, string, boolean?, (() => boolean)?][] = [
["tray", "Tray Icon", "Add a tray icon for Vencord Desktop", true],
[
@@ -22,6 +26,7 @@ export default function SettingsUi() {
true,
() => Settings.tray ?? true
],
["arRPC", "Rich Presence", "Enables Rich Presence via arRPC", false],
[
"disableMinSize",
"Disable minimum window size",
@@ -57,6 +62,29 @@ export default function SettingsUi() {
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
<Switch
value={autoStartEnabled}
onChange={async v => {
await autostart[v ? "enable" : "disable"]();
setAutoStartEnabled(v);
}}
note="Automatically start Vencord Desktop on computer start-up"
>
Start With System
</Switch>
<Switch
value={Settings.appBadge ?? true}
onChange={v => {
Settings.appBadge = v;
if (v) setBadge();
else VencordDesktopNative.app.setBadgeCount(0);
}}
note="Show mention badge on the app icon"
>
Notification Badge
</Switch>
{switches.map(([key, text, note, def, predicate]) => (
<Switch
value={(Settings[key as any] ?? def ?? false) && predicate?.() !== false}

View File

@@ -4,4 +4,5 @@
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
export * as ScreenShare from "./ScreenSharePicker";
export { default as Settings } from "./Settings";

View File

@@ -0,0 +1,126 @@
.vcd-screen-picker-modal {
padding: 1em;
}
.vcd-screen-picker-header h1 {
margin: 0;
}
.vcd-screen-picker-footer {
display: flex;
gap: 1em;
}
.vcd-screen-picker-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2em 1em;
}
.vcd-screen-picker-grid input {
appearance: none;
cursor: pointer;
}
.vcd-screen-picker-selected img {
border: 2px solid var(--brand-experiment);
border-radius: 3px;
}
.vcd-screen-picker-grid label {
overflow: hidden;
padding: 4px 0px;
cursor: pointer;
}
.vcd-screen-picker-grid label:hover {
outline: 2px solid var(--brand-experiment);
}
.vcd-screen-picker-grid div {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: center;
font-weight: 600;
margin-inline: 0.5em;
}
.vcd-screen-picker-card {
padding: 0.5em;
box-sizing: border-box;
}
.vcd-screen-picker-preview img {
width: 100%;
margin-bottom: 0.5em;
}
.vcd-screen-picker-preview {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 1em;
}
.vcd-screen-picker-radio input {
display: none;
}
.vcd-screen-picker-radio {
background-color: var(--background-secondary);
border: 1px solid var(--primary-800);
padding: 0.3em;
cursor: pointer;
}
.vcd-screen-picker-radio h2 {
margin: 0;
}
.vcd-screen-picker-radio[data-checked="true"] {
background-color: var(--brand-experiment);
border-color: var(--brand-experiment);
}
.vcd-screen-picker-radio[data-checked="true"] h2 {
color: var(--interactive-active);
}
.vcd-screen-picker-quality {
display: flex;
gap: 1em;
margin-bottom: 0.5em;
opacity: 0.3;
}
.vcd-screen-picker-quality section {
flex: 1 1 auto;
}
.vcd-screen-picker-radios {
display: flex;
width: 100%;
border-radius: 3px;
}
.vcd-screen-picker-radios label {
flex: 1 1 auto;
text-align: center;
}
.vcd-screen-picker-radios label:first-child {
border-radius: 3px 0 0 3px;
}
.vcd-screen-picker-radios label:last-child {
border-radius: 0 3px 3px 0;
}
.vcd-screen-picker-audio {
margin-bottom: 0;
}

View File

@@ -22,11 +22,11 @@ Object.defineProperty(Notification.prototype, "onclick", {
configurable: true
});
// Enable Desktop Notifications by default
if (isFirstRun) {
// Hide "Download Discord Desktop now!!!!" banner
localStorage.setItem("hideNag", "true");
// Enable Desktop Notifications by default
waitFor("setDesktopType", m => {
m.setDesktopType("all");
});

View File

@@ -5,8 +5,23 @@
*/
import "./fixes";
import "./appBadge";
import "./patches";
console.log("read if cute :3");
export * as Components from "./components";
export { Settings } from "./settings";
import { Settings } from "./settings";
export { Settings };
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"];
arRPC.required = !!Settings.store.arRPC;
Settings.addChangeListener("arRPC", v => {
arRPC.required = !!v;
if (v && !arRPC.started) Vencord.Plugins.startPlugin(arRPC);
else if (arRPC.started) {
Vencord.Plugins.stopPlugin(arRPC);
}
});

View File

@@ -0,0 +1,8 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
// TODO: Possibly auto generate glob if we have more patches in the future
import "./spellCheck";

View File

@@ -0,0 +1,30 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Patch } from "@vencord/types/utils/types";
window.VCDP = {};
interface PatchData {
patches: Omit<Patch, "plugin">[];
[key: string]: any;
}
export function addPatch<P extends PatchData>(p: P) {
const { patches, ...globals } = p;
for (const patch of patches as Patch[]) {
if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement];
for (const r of patch.replacement) {
if (typeof r.replace === "string") r.replace = r.replace.replaceAll("$self", "VCDP");
}
patch.plugin = "VencordDesktop";
Vencord.Plugins.patches.push(patch);
}
Object.assign(VCDP, globals);
}

View File

@@ -0,0 +1,83 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { addContextMenuPatch } from "@vencord/types/api/ContextMenu";
import { findStoreLazy } from "@vencord/types/webpack";
import { ContextMenu, FluxDispatcher, Menu } from "@vencord/types/webpack/common";
import { addPatch } from "./shared";
let word: string;
let corrections: string[];
const SpellCheckStore = findStoreLazy("SpellcheckStore");
// Make spellcheck suggestions work
addPatch({
patches: [
{
find: ".enableSpellCheck)",
replacement: {
// if (isDesktop) { DiscordNative.onSpellcheck(openMenu(props)) } else { e.preventDefault(); openMenu(props) }
match: /else\{(.{1,3})\.preventDefault\(\);(.{1,3}\(.{1,3}\))\}(?<=:(.{1,3})\.enableSpellCheck\).+?)/,
// ... else { $self.onSlateContext(() => openMenu(props)) }
replace: "else {$self.onSlateContext($1, $3?.enableSpellCheck, () => $2)}"
}
}
],
onSlateContext(e: MouseEvent, hasSpellcheck: boolean | undefined, openMenu: () => void) {
if (!hasSpellcheck) {
e.preventDefault();
openMenu();
return;
}
const cb = (w: string, c: string[]) => {
VencordDesktopNative.spellcheck.offSpellcheckResult(cb);
word = w;
corrections = c;
openMenu();
};
VencordDesktopNative.spellcheck.onSpellcheckResult(cb);
}
});
addContextMenuPatch("textarea-context", children => () => {
const hasCorrections = Boolean(word && corrections?.length);
children.push(
<Menu.MenuGroup>
{hasCorrections && (
<>
{corrections.map(c => (
<Menu.MenuItem
id={"vcd-spellcheck-suggestion-" + c}
label={c}
action={() => VencordDesktopNative.spellcheck.replaceMisspelling(c)}
/>
))}
<Menu.MenuSeparator />
<Menu.MenuItem
id="vcd-spellcheck-learn"
label={`Add ${word} to dictionary`}
action={() => VencordDesktopNative.spellcheck.addToDictionary(word)}
/>
</>
)}
<Menu.MenuCheckboxItem
id="vcd-spellcheck-enabled"
label="Enable Spellcheck"
checked={SpellCheckStore.isEnabled()}
action={() => {
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
// Haven't found a good way to update state, so just close for now 🤷‍♀️
ContextMenu.close();
}}
/>
</Menu.MenuGroup>
);
});

View File

@@ -4,7 +4,7 @@
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
export const localStorage = (window.vcdLS = window.localStorage);
export const { localStorage } = window;
export const isFirstRun = (() => {
const key = "VCD_FIRST_RUN";
@@ -12,3 +12,7 @@ export const isFirstRun = (() => {
localStorage.setItem(key, "false");
return true;
})();
const { platform } = navigator;
export const isWindows = platform.startsWith("Win");

View File

@@ -10,11 +10,12 @@ export const enum IpcEvents {
GET_RENDERER_SCRIPT = "VCD_GET_RENDERER_SCRIPT",
GET_RENDERER_CSS_FILE = "VCD_GET_RENDERER_CSS_FILE",
RELAUNCH = "VCD_RELAUNCH",
FOCUS = "VCD_FOCUS",
GET_VERSION = "VCD_GET_VERSION",
RELAUNCH = "VCD_RELAUNCH",
CLOSE = "VCD_CLOSE",
FOCUS = "VCD_FOCUS",
SHOW_ITEM_IN_FOLDER = "VCD_SHOW_ITEM_IN_FOLDER",
GET_SETTINGS = "VCD_GET_SETTINGS",
SET_SETTINGS = "VCD_SET_SETTINGS",
@@ -26,6 +27,15 @@ export const enum IpcEvents {
UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES",
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING",
SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",
CLOSE = "VCD_CLOSE"
SET_BADGE_COUNT = "VCD_SET_BADGE_COUNT",
CAPTURER_GET_LARGE_THUMBNAIL = "VCD_CAPTURER_GET_LARGE_THUMBNAIL",
AUTOSTART_ENABLED = "VCD_AUTOSTART_ENABLED",
ENABLE_AUTOSTART = "VCD_ENABLE_AUTOSTART",
DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART"
}

View File

@@ -7,4 +7,6 @@
import { join } from "path";
export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views");
export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");
export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png");

View File

@@ -18,4 +18,8 @@ export interface Settings {
minimizeToTray?: boolean;
skippedUpdate?: string;
staticTitle?: boolean;
arRPC?: boolean;
appBadge?: boolean;
firstLaunch?: boolean;
}

View File

@@ -1,23 +0,0 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
type Func = (...args: any[]) => any;
export function monkeyPatch<O extends object>(
object: O,
key: keyof O,
replacement: (original: Func, ...args: any[]) => any
): void {
const original = object[key] as Func;
const replacer = (object[key] = function (this: unknown, ...args: any[]) {
return replacement.call(this, original, ...args);
} as any);
Object.defineProperties(replacer, Object.getOwnPropertyDescriptors(original));
replacer.toString = () => original.toString();
replacer.$$original = original;
}

View File

@@ -6,11 +6,11 @@
import { app, BrowserWindow, ipcMain, shell } from "electron";
import { Settings } from "main/settings";
import { makeLinksOpenExternally } from "main/utils/makeLinksOpenExternally";
import { githubGet, ReleaseData } from "main/utils/vencordLoader";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
import { VIEW_DIR } from "shared/paths";
export interface UpdateData {
currentVersion: string;
@@ -22,11 +22,18 @@ let updateData: UpdateData;
ipcMain.handle(IpcEvents.UPDATER_GET_DATA, () => updateData);
ipcMain.handle(IpcEvents.UPDATER_DOWNLOAD, () => {
const portable = !!process.env.PORTABLE_EXECUTABLE_FILE;
const { assets } = updateData.release;
const url = (() => {
switch (process.platform) {
case "win32":
return assets.find(a => a.name.endsWith(".exe"))!.browser_download_url;
return assets.find(a => {
if (!a.name.endsWith(".exe")) return false;
const isSetup = a.name.includes("Setup");
return portable ? !isSetup : isSetup;
})!.browser_download_url;
case "darwin":
return assets.find(a => a.name.endsWith(".dmg"))!.browser_download_url;
case "linux":
@@ -72,13 +79,13 @@ export async function checkUpdates() {
const oldVersion = app.getVersion();
const newVersion = data.tag_name.replace(/^v/, "");
if (Settings.store.skippedUpdate !== newVersion && isOutdated(oldVersion, newVersion)) {
updateData = {
currentVersion: oldVersion,
latestVersion: newVersion,
release: data
};
updateData = {
currentVersion: oldVersion,
latestVersion: newVersion,
release: data
};
if (Settings.store.skippedUpdate !== newVersion && isOutdated(oldVersion, newVersion)) {
openNewUpdateWindow();
}
} catch (e) {
@@ -88,11 +95,18 @@ export async function checkUpdates() {
function openNewUpdateWindow() {
const win = new BrowserWindow({
...SplashProps,
width: 500,
autoHideMenuBar: true,
alwaysOnTop: true,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
preload: join(__dirname, "updaterPreload.js"),
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
});
win.loadFile(join(STATIC_DIR, "updater.html"));
makeLinksOpenExternally(win);
win.loadFile(join(VIEW_DIR, "updater.html"));
}

View File

@@ -5,7 +5,7 @@
*/
import { contextBridge } from "electron";
import { invoke } from "preload/typedIpcs";
import { invoke } from "preload/typedIpc";
import { IpcEvents } from "shared/IpcEvents";
import type { UpdateData } from "./main";

View File

@@ -1,34 +0,0 @@
<head>
<style>
body {
padding: 2em;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
}
h1 {
text-align: center;
}
</style>
</head>
<body>
<h1>About Vencord Desktop</h1>
<p>
Vencord Desktop is a free/libre cross platform desktop app aiming to give you a snappier Discord experience with
Vencord pre-installed
</p>
<h2>Links</h2>
<ul>
<li>
<a href="https://vencord.dev">Vencord Website</a>
</li>
<li>
<a href="https://github.com/Vencord/Desktop" target="_blank">Source Code</a>
</li>
<li>
<a href="https://github.com/Vencord/Desktop/issues" target="_blank">Report bugs / Request features</a>
</li>
</ul>
</body>

BIN
static/badges/1.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/10.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/11.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/2.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/3.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/4.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/5.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/6.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/7.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/8.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/9.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

71
static/views/about.html Normal file
View File

@@ -0,0 +1,71 @@
<head>
<link rel="stylesheet" href="./style.css" type="text/css" />
<style>
body {
padding: 2em;
}
h1 {
text-align: center;
}
</style>
</head>
<body>
<h1 id="title">Vencord Desktop</h1>
<p>
Vencord Desktop is a free/libre cross platform desktop app aiming to give you a snappier Discord experience with
Vencord pre-installed
</p>
<section>
<h2>Links</h2>
<ul>
<li>
<a href="https://vencord.dev" target="_blank">Vencord Website</a>
</li>
<li>
<a href="https://github.com/Vencord/Desktop" target="_blank">Source Code</a>
</li>
<li>
<a href="https://github.com/Vencord/Desktop/issues" target="_blank">Report bugs / Request features</a>
</li>
</ul>
</section>
<section>
<h2>Acknowledgements</h2>
<p>These awesome libraries empower Vencord Desktop</p>
<ul>
<li>
<a href="https://github.com/electron/electron" target="_blank">Electron</a>
- Build cross-platform desktop apps with JavaScript, HTML, and CSS
</li>
<li>
<a href="https://github.com/electron-userland/electron-builder" target="_blank">Electron Builder</a>
- A complete solution to package and build a ready for distribution Electron app with “auto update”
support out of the box
</li>
<li>
<a href="https://github.com/OpenAsar/arrpc" target="_blank">arrpc</a>
- An open implementation of Discord's Rich Presence server
</li>
<li>
And many
<a href="https://github.com/Vencord/Desktop/blob/main/pnpm-lock.yaml" target="_blank"
>more awesome open source libraries</a
>
</li>
</ul>
</section>
</body>
<script type="module">
const data = await Updater.getData();
if (data.currentVersion) {
const title = document.getElementById("title");
title.textContent += ` v${data.currentVersion}`;
}
</script>

View File

@@ -0,0 +1,170 @@
<head>
<link rel="stylesheet" href="./style.css" type="text/css" />
<style>
body {
height: 100vh;
padding: 1.5em;
padding-bottom: 1em;
border: 1px solid var(--fg-semi-trans);
border-top: none;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
select {
background: var(--bg);
color: var(--fg);
padding: 0.3em;
margin: -0.3em;
border-radius: 6px;
}
h1 {
margin: 0.4em 0 0;
}
p {
margin: 1em 0 2em;
}
form {
display: grid;
gap: 1em;
margin: 0;
}
label {
position: relative;
display: flex;
justify-content: space-between;
}
label:has(input[type="checkbox"]),
select {
cursor: pointer;
}
label:not(:last-child)::after {
content: "";
position: absolute;
bottom: -10px;
width: 100%;
height: 1px;
background-color: var(--fg-secondary);
opacity: 0.5;
}
label div {
display: grid;
gap: 0.2em;
}
label h2 {
margin: 0;
font-weight: normal;
font-size: 1.1rem;
line-height: 1rem;
}
label span {
font-size: 0.9rem;
font-weight: 400;
color: var(--fg-secondary);
}
#buttons {
display: flex;
justify-content: end;
gap: 0.5em;
margin-top: auto;
}
button {
padding: 0.6em;
background: red;
color: white;
border-radius: 6px;
border: none;
cursor: pointer;
transition: 200ms filter;
}
button:hover {
filter: brightness(0.8);
}
#submit {
background: green;
}
</style>
</head>
<body>
<h1>Welcome to Vencord Desktop</h1>
<p>Let's customise your experience!</p>
<form>
<label>
<h2>Discord Branch</h2>
<select name="discordBranch">
<option value="stable">stable</option>
<option value="canary">canary</option>
<option value="ptb">ptb</option>
</select>
</label>
<label>
<div>
<h2>Start with System</h2>
<span>Automatically open Vencord Desktop when your computer starts</span>
</div>
<input type="checkbox" name="autoStart" />
</label>
<label>
<div>
<h2>Rich Presence</h2>
<span
>Enable Rich presence (game activity) via
<a href="https://github.com/OpenAsar/arrpc" target="_blank">arRPC</a></span
>
</div>
<input type="checkbox" name="richPresence" checked />
</label>
<label>
<div>
<h2>Import Settings</h2>
<span>Import Settings from existing Vencord install (if found)</span>
</div>
<input type="checkbox" name="importSettings" checked />
</label>
<label>
<div>
<h2>Minimise to Tray</h2>
<span>Minimise to Tray when closing</span>
</div>
<input type="checkbox" name="minimizeToTray" checked />
</label>
</form>
<div id="buttons">
<button id="cancel">Quit</button>
<button id="submit">Submit</button>
</div>
</body>
<script>
cancel.onclick = () => console.info("cancel");
submit.onclick = e => {
const form = document.querySelector("form");
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
console.info("form:" + JSON.stringify(data));
e.preventDefault();
};
</script>

View File

@@ -1,16 +1,11 @@
<head>
<link rel="stylesheet" href="./style.css" type="text/css" />
<style>
* {
user-select: none;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
margin: 0;
padding: 0;
}
.wrapper {
box-sizing: border-box;
height: 100%;
@@ -18,13 +13,11 @@
flex-direction: column;
justify-content: center;
align-items: center;
background-color: hsl(223 6.7% 20.6%);
border-radius: 8px;
border: 1px solid hsl(220 6.5% 18%);
border: 1px solid var(--fg-semi-trans);
}
p {
color: rgb(219, 222, 225);
text-align: center;
}

30
static/views/style.css Normal file
View File

@@ -0,0 +1,30 @@
:root {
--bg: white;
--fg: black;
--fg-secondary: #313338;
--fg-semi-trans: rgb(0 0 0 / 0.2);
--link: #006ce7;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(223 6.7% 20.6%);
--fg: white;
--fg-secondary: #b5bac1;
--fg-semi-trans: rgb(255 255 255 / 0.2);
--link: #00a8fc;
}
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
}
a {
color: var(--link);
}

View File

@@ -1,22 +1,13 @@
<head>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
margin: 0;
padding: 0;
color: rgb(219, 222, 225);
}
<link rel="stylesheet" href="./style.css" type="text/css" />
<style>
.wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
height: 100%;
background-color: #313338;
border-radius: 8px;
border: 1px solid #248046;
min-height: 100%;
padding: 1em;
}
@@ -34,7 +25,7 @@
button {
cursor: pointer;
padding: 0.5em;
color: white;
color: var(--fg);
border: none;
border-radius: 3px;
font-weight: bold;
@@ -66,6 +57,9 @@
<br />
Latest: <span id="latest"></span>
</p>
<h2>Changelog</h2>
<p id="changelog">Loading...</p>
</section>
<section>
@@ -113,3 +107,17 @@
});
}
</script>
<script type="module">
import { micromark } from "https://esm.sh/micromark@3?bundle";
import { gfm, gfmHtml } from "https://esm.sh/micromark-extension-gfm@2?bundle";
const changelog = (await Updater.getData()).release.body;
if (changelog)
document.getElementById("changelog").innerHTML = micromark(changelog, {
extensions: [gfm()],
htmlExtensions: [gfmHtml()]
})
.replace(/h1>/g, "h3>")
.replace(/<a /g, '<a target="_blank" ');
</script>