70 Commits

Author SHA1 Message Date
Vendicated
4afafa5038 bump 2023-04-15 12:38:17 +02:00
Ryan Cao
583680d311 feat: reuse built-in menus in system menubar (#12)
* fix: add edit menu on macOS to allow clipboard actions

* feat: reuse more built-in menus

* re-add zoom shortcut fix

---------

Co-authored-by: V <vendicated@riseup.net>
2023-04-15 10:37:52 +00:00
Vendicated
70dd38f79d make spellcheck use all system locales 2023-04-14 04:05:56 +02:00
Sofia Lima
9120d05efc fix makepkg in AUR workflow again (#11) 2023-04-11 05:43:12 +02:00
Vendicated
3a820b458f Fix ssh shenanigans 2023-04-11 02:12:10 +02:00
Sofia Lima
47f3b7fc89 fix AUR workflow (#10) 2023-04-11 01:44:15 +02:00
Vendicated
838a3c78dd Bump to 0.1.6 2023-04-10 22:54:09 +02:00
V
8d51cd5029 Add basic update notifications (#9) 2023-04-10 22:53:44 +02:00
Vendicated
bfb9af05b0 Fix Settings not persisting, ooop 2023-04-10 22:30:39 +02:00
Vendicated
897df3a5d4 Add support for (optional) GITHUB_TOKEN env variable 2023-04-10 22:05:21 +02:00
Vendicated
d1296d1708 Windows: Fix taskbar icon getting removed on update 2023-04-10 21:37:14 +02:00
Vendicated
c5165afe7f Unexplode release workflow 2023-04-10 19:38:32 +02:00
Vendicated
e16cdcf387 Bump to 0.1.5 2023-04-10 19:27:14 +02:00
Vendicated
1c2ed4679d Fix custom vencord location 2023-04-10 19:26:59 +02:00
Ryan Cao
af42af070e feat: implement translucency for macOS (#4)
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-04-10 19:21:45 +02:00
Vendicated
07d647ba2e Fix outdated code 2023-04-10 19:20:16 +02:00
Vendicated
90b8c5fdfe lint 2023-04-10 19:18:00 +02:00
Vendicated
c0598821ee Add test workflow 2023-04-10 19:17:01 +02:00
Kode
de3aae1d95 Tray: Option to hide (#7)
Co-authored-by: V <vendicated@riseup.net>
2023-04-10 19:12:58 +02:00
Vendicated
d1acb0490b small fix 2023-04-10 03:26:55 +02:00
Vendicated
60759c142e Increase window size on enable min size 2023-04-10 01:55:48 +02:00
Vendicated
0c77dbec92 Improve SettingsStore, add disableMinSize listener 2023-04-10 01:43:47 +02:00
Vendicated
edfeca15ce add some jsdoc 2023-04-10 01:16:44 +02:00
Vendicated
c2eaa9d35a Rewrite settings proxy to proper store 2023-04-10 01:04:41 +02:00
Vendicated
ddebb6563a apply lint 2023-04-09 22:55:12 +02:00
Vendicated
df53ea549a Add eslint config 2023-04-09 22:55:12 +02:00
Vendicated
a4954570b6 smaller version 2023-04-09 22:31:17 +02:00
Vendicated
ca376d35ab Important splash screen fix 2023-04-09 22:30:34 +02:00
Sofia Lima
db76f111c8 add workflow for updating AUR package (#6) 2023-04-09 21:36:05 +02:00
Vendicated
5efbbe2106 Improve settings texts 2023-04-09 06:05:52 +02:00
Vendicated
c29dd6d2d7 Add option to not minimize to tray 2023-04-09 05:57:45 +02:00
Vendicated
94b80ebe75 Fix comment location 2023-04-09 05:26:50 +02:00
Vendicated
bba8899f67 Implement frameless & windows ctrl q settings 2023-04-09 05:25:45 +02:00
Vendicated
ba0e8fedd0 Add minimum window height & width 2023-04-09 05:04:58 +02:00
V
27b6264c79 Update README.md 2023-04-09 04:35:27 +02:00
V
81929f1733 Add AUR package to README 2023-04-09 04:35:04 +02:00
Vendicated
d7e2dd0593 Remove pacman target its kinda scam 2023-04-09 04:23:52 +02:00
Vendicated
a807ae85fc bump! 2023-04-09 04:11:22 +02:00
Vendicated
cb2fb648b6 Fix file downloader 2023-04-09 03:10:24 +02:00
Vendicated
e265e70fb9 fixes 2023-04-09 03:06:19 +02:00
Vendicated
805b6fbcc4 Add ability to load Vencord from custom location (dev install) 2023-04-09 02:26:31 +02:00
Vendicated
591d380160 Turn IpcEvents into a const enum 2023-04-09 01:22:31 +02:00
Vendicated
8f19d41d4c Not infuriating at all 2023-04-09 01:20:35 +02:00
Vendicated
b4a2c41e74 VencordDesktop -> VencordDesktopNative; add VencordDesktop global 2023-04-09 01:20:00 +02:00
Vendicated
7e0532444d Initial Settings UI work 2023-04-09 00:49:47 +02:00
Vendicated
8b68eef9a7 linux: Add .desktop file 2023-04-06 17:05:36 +02:00
Nick
e3f973ff68 chore: add missing homepage url (#3)
* Update package.json

* Update package.json
2023-04-06 06:32:19 +02:00
Vendicated
34eb14f9ed i hate workflows 2023-04-06 05:14:28 +02:00
Vendicated
44438441d6 Bump to v0.1.3 2023-04-06 03:52:22 +02:00
Vendicated
bbe670fc92 Tray: Add separator 2023-04-06 03:52:06 +02:00
Toad
3da4c02129 Tray: Add more buttons (#2) 2023-04-06 00:08:28 +02:00
Vendicated
19c43289f6 Make QuickCSS work 2023-04-05 20:01:31 +02:00
Vendicated
1980606e03 Add about page & setting to open links with electron 2023-04-05 17:55:49 +02:00
Vendicated
17150503d2 Add Menu 2023-04-05 17:25:29 +02:00
Vendicated
fe58dcfa51 package.json cleanup 2023-04-05 16:57:09 +02:00
Vendicated
f357ee4260 forgor to add tsconfig 2023-04-05 16:56:33 +02:00
Vendicated
9d144a11be Enable Desktop Notifications on first run 2023-04-05 16:51:19 +02:00
Vendicated
d9f2b15e84 Hide Download Desktop app button 2023-04-05 05:31:44 +02:00
Vendicated
cf23b6d028 Make clicking notifications focus VC Desktop 2023-04-05 05:19:48 +02:00
Vendicated
2efed86b71 v0.1.2 2023-04-05 04:36:37 +02:00
Vendicated
816ba5d1d0 Make it possible to reopen app after close to tray 2023-04-05 04:25:17 +02:00
Vendicated
91e1cc02ce windows: fix notification title & icon 2023-04-05 04:20:13 +02:00
Vendicated
8630f79b06 make nsis installer configurable 2023-04-05 04:20:02 +02:00
Vendicated
79d73e23c0 Init renderer, expose settings via ipc, init canary/ptb support 2023-04-04 04:40:03 +02:00
Vendicated
18a77d45b8 Remember window position & size 2023-04-04 04:17:38 +02:00
Vendicated
f9ebc16656 Add Build & Install instructions 2023-04-04 03:35:31 +02:00
Vendicated
0296f78731 Bump!! 2023-04-04 03:01:21 +02:00
Vendicated
b151631d03 make crapple happy 2: Electric Boogaloo 2023-04-04 03:01:11 +02:00
Vendicated
d817950681 make crapple happy 2023-04-04 02:53:09 +02:00
Vendicated
c0ba813041 switch to png icon 2023-04-04 02:26:55 +02:00
60 changed files with 4058 additions and 539 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# githubs api has a rate limit of 60/h if not authorised.
# you may quickly hit that and get rate limited. To counteract this, you can provide a github token
# here and it will be used. To do so, create a token at the following links and just leave
# all permissions at the defaults (public repos read only, 0 permissions):
# https://github.com/settings/personal-access-tokens/new
GITHUB_TOKEN=

63
.eslintrc.json Normal file
View File

@@ -0,0 +1,63 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "node_modules"],
"plugins": [
"@typescript-eslint",
"license-header",
"simple-import-sort",
"unused-imports",
"path-alias",
"prettier"
],
"settings": {
"import/resolver": {
"alias": {
"map": []
}
}
},
"rules": {
"license-header/header": ["error", "scripts/header.txt"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", { "object": true, "array": false }],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"dot-notation": "error",
"no-useless-escape": "error",
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error",
"prettier/prettier": "error"
}
}

View File

@@ -1,25 +1,44 @@
name: Build/release
name: Release
on: push
on:
push:
tags:
- v*
jobs:
release:
runs-on: ${{ matrix.os }}
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v3
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Run Electron Builder
uses: samuelmeuli/action-electron-builder@e4b12cd06ddf023422f1ac4e39632bd76f6e6928
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') }}
- name: Run Electron Builder
uses: samuelmeuli/action-electron-builder@e4b12cd06ddf023422f1ac4e39632bd76f6e6928
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE: true
- name: Update AUR package
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
mkdir -p -m 700 ~/.ssh
echo $SSH_KEY > ~/.ssh/aur
echo $SSH_PUB_KEY > ~/.ssh/aur.pub
chmod 600 ~/.ssh/*
export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur"
sudo ./scripts/ci/install_makepkg.sh
./scripts/ci/aur_bump.sh
env:
SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
SSH_PUB_KEY: ${{ secrets.AUR_SSH_PUB_KEY }}

32
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test
- name: Test if it compiles
run: |
pnpm build
pnpm build --dev

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
dist
node_modules
.env

View File

@@ -5,4 +5,4 @@ trailingComma: none
bracketSpacing: true
arrowParens: avoid
useTabs: false
endOfLine: auto
endOfLine: lf

18
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@@ -1,6 +1,60 @@
# Vencord Desktop
A standalone Electron app that loads Discord & Vencord (very early and unfinished)
A standalone Electron app that loads Discord & Vencord
Vencord Desktop is currently in very early alpha. Bug reports, feature requests & contributions are highly appreciated!!
## Installing
### Windows
Download and run Vencord-Desktop-Setup-VERSION.exe from [releases](https://github.com/Vencord/Desktop/releases/latest)
### Mac
Download and run Vencord-Desktop-VERSION.dmg from [releases](https://github.com/Vencord/Desktop/releases/latest)
### Linux
#### Arch based
Install [vencord-desktop-git](https://aur.archlinux.org/packages/vencord-desktop-git) from the AUR using your favourite AUR helper, for example [yay](https://github.com/Jguer/yay)
#### Ubuntu/Debian based
Download Vencord-Desktop-VERSION.deb from [releases](https://github.com/Vencord/Desktop/releases/latest)
#### Fedora/RHEL based
Download Vencord-Desktop-VERSION.rpm from [releases](https://github.com/Vencord/Desktop/releases/latest)
#### Other
Either download Vencord-Desktop-VERSION.AppImage and just run it directly or grab Vencord-Desktop-VERSION.tar.gz, extract it somewhere and run `vencorddesktop`.
A flatpak is planned, if you want packages for other repos, feel free to create them and they can be linked as unofficial here
## Building
Packaging will create builds in the dist/ folder. You can then install them like mentioned above or distribute them
```sh
git clone https://github.com/Vencord/Desktop
cd Desktop
# Install Dependencies
pnpm i
# Either run it without packaging
pnpm start
# Or package
pnpm package
# Or only build the pacman target
pnpm package --linux pacman
# Or package to a directory only
pnpm package:dir
```
## Motivation

View File

@@ -1,38 +1,65 @@
{
"name": "vencorddesktop",
"name": "VencordDesktop",
"version": "0.1.7",
"private": true,
"version": "0.1.0",
"description": "",
"keywords": [],
"homepage": "https://vencord.dev/",
"license": "GPL-3.0",
"author": "Vendicated <vendicated@riseup.net>",
"main": "dist/js/main.js",
"scripts": {
"build": "tsx scripts/build.mts",
"watch": "pnpm build --watch",
"build": "tsx scripts/build/build.mts",
"build:dev": "pnpm build --dev",
"package": "pnpm build && electron-builder",
"package:dir": "pnpm build && electron-builder --dir",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.mts,.mjs",
"lint:fix": "pnpm lint --fix",
"start": "pnpm build && electron .",
"start:watch": "tsx scripts/startWatch.mts",
"test": "echo \"Error: no test specified\" && exit 1"
"start:dev": "pnpm build:dev && electron .",
"start:watch": "pnpm build:dev && tsx scripts/startWatch.mts",
"test": "pnpm lint && pnpm testTypes",
"testTypes": "tsc --noEmit",
"watch": "pnpm build --watch"
},
"keywords": [],
"author": "Vendicated <vendicated@riseup.net>",
"license": "GPL-3.0",
"devDependencies": {
"@types/node": "^18.15.11",
"@types/react": "^18.0.33",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"dotenv": "^16.0.3",
"electron": "^23.2.0",
"electron-builder": "^23.6.0",
"esbuild": "^0.17.14",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-license-header": "^0.6.0",
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.8.7",
"source-map-support": "^0.5.21",
"tsx": "^3.12.6",
"type-fest": "^3.8.0",
"typescript": "^5.0.2"
},
"packageManager": "pnpm@8.1.1",
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"build": {
"appId": "dev.vencord.desktop",
"productName": "Vencord Desktop",
"mac": {
"category": "Network"
},
"nsis": {
"include": "build/installer.nsh"
},
"files": [
"!*",
"dist/js",
"static",
"package.json",
"LICENSE"
],
"linux": {
"category": "Network",
"maintainer": "vendicated+vencord-desktop@riseup.net",
@@ -41,14 +68,21 @@
"tar.gz",
"rpm",
"AppImage"
]
],
"desktop": {
"Name": "Vencord Desktop",
"GenericName": "Internet Messenger",
"Type": "Application",
"Categories": "Network;InstantMessaging;Chat;",
"Keywords": "discord;vencord;electron;chat;"
}
},
"files": [
"!*",
"dist/js",
"static",
"package.json",
"LICENSE"
]
"mac": {
"category": "Network"
},
"nsis": {
"include": "build/installer.nsh",
"oneClick": false
}
}
}

2460
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
import { BuildContext, BuildOptions, context } from "esbuild";
const NodeCommonOpts: BuildOptions = {
format: "cjs",
platform: "node",
external: ["electron"],
minify: true,
bundle: true,
sourcemap: "linked",
logLevel: "info"
};
const contexts = [] as BuildContext[];
async function createContext(options: BuildOptions) {
contexts.push(await context(options));
}
await Promise.all([
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/js/main.js"
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/preload/index.ts"],
outfile: "dist/js/preload.js"
})
]);
const watch = process.argv.includes("--watch");
if (watch) {
await Promise.all(contexts.map(ctx => ctx.watch()));
} else {
await Promise.all(contexts.map(async ctx => {
await ctx.rebuild();
await ctx.dispose();
}));
}

75
scripts/build/build.mts Normal file
View File

@@ -0,0 +1,75 @@
/*
* 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 { BuildContext, BuildOptions, context } from "esbuild";
const isDev = process.argv.includes("--dev");
const CommonOpts: BuildOptions = {
minify: !isDev,
bundle: true,
sourcemap: "linked",
logLevel: "info"
};
const NodeCommonOpts: BuildOptions = {
...CommonOpts,
format: "cjs",
platform: "node",
external: ["electron"],
target: ["esnext"],
define: {
IS_DEV: JSON.stringify(isDev)
}
};
const contexts = [] as BuildContext[];
async function createContext(options: BuildOptions) {
contexts.push(await context(options));
}
await Promise.all([
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/js/main.js"
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/preload/index.ts"],
outfile: "dist/js/preload.js"
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/updater/preload.ts"],
outfile: "dist/js/updaterPreload.js"
}),
createContext({
...CommonOpts,
globalName: "VencordDesktop",
entryPoints: ["src/renderer/index.ts"],
outfile: "dist/js/renderer.js",
format: "iife",
inject: ["./scripts/build/injectReact.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
})
]);
const watch = process.argv.includes("--watch");
if (watch) {
await Promise.all(contexts.map(ctx => ctx.watch()));
} else {
await Promise.all(
contexts.map(async ctx => {
await ctx.rebuild();
await ctx.dispose();
})
);
}

View File

@@ -0,0 +1,9 @@
/*
* 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
*/
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
export let VencordCreateElement = (...args) =>
(VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View File

@@ -0,0 +1,7 @@
// Work around https://github.com/evanw/esbuild/issues/2460
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

20
scripts/ci/aur_bump.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
set -e
VERSION=$(git describe --tags --abbrev=0 | tr -d 'v')
SHASUM=$(sha256sum "dist/VencordDesktop-$VERSION.tar.gz" | awk '{ print $1 }')
git clone ssh://aur@aur.archlinux.org/vencord-desktop-bin.git aurpkg
cd aurpkg
sed -i "s/^pkgver=.*$/pkgver=$VERSION/" PKGBUILD
sed -i "s/^sha256sums=('.*'/sha256sums=('$SHASUM'/" PKGBUILD
makepkg --printsrcinfo > .SRCINFO
git commit -a -m "Bump version to $VERSION"
git push
cd ..
rm -rf aurpkg

13
scripts/ci/install_makepkg.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
for i in \
"makepkg_6.0.2-3_amd64.deb" \
"libalpm13_13.0.2-3_amd64.deb" \
"pacman-package-manager_6.0.2-3_amd64.deb"; do
wget -O/tmp/$i https://fr.archive.ubuntu.com/ubuntu/pool/universe/p/pacman-package-manager/$i
dpkg -i /tmp/$i || true
done
apt-get -f install -oDpkg::Use-Pty=0 -qq

5
scripts/header.txt Normal file
View File

@@ -0,0 +1,5 @@
/*
* 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
*/

11
scripts/start.ts Normal file
View File

@@ -0,0 +1,11 @@
/*
* 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 "./utils/dotenv";
import { spawnNodeModuleBin } from "./utils/spawn.mjs";
spawnNodeModuleBin("electron", ["."]);

View File

@@ -1,15 +1,10 @@
import { spawn as cpSpawn, SpawnOptions } from "child_process";
import { join } from "path";
/*
* 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
*/
const EXT = process.platform === "win32" ? ".cmd" : "";
import "./start";
const OPTS: SpawnOptions = {
stdio: "inherit",
};
function spawn(bin: string, args: string[]) {
cpSpawn(join("node_modules", ".bin", bin + EXT), args, OPTS);
}
spawn("tsx", ["scripts/build.mts", "--", "--watch"]);
spawn("electron", ["."]);
import { spawnNodeModuleBin } from "./utils/spawn.mjs";
spawnNodeModuleBin("tsx", ["scripts/build/build.mts", "--", "--watch", "--dev"]);

9
scripts/utils/dotenv.ts Normal file
View File

@@ -0,0 +1,9 @@
/*
* 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 { config } from "dotenv";
config();

18
scripts/utils/spawn.mts Normal file
View File

@@ -0,0 +1,18 @@
/*
* 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 { spawn as spaaawn, SpawnOptions } from "child_process";
import { join } from "path";
const EXT = process.platform === "win32" ? ".cmd" : "";
const OPTS: SpawnOptions = {
stdio: "inherit"
};
export function spawnNodeModuleBin(bin: string, args: string[]) {
spaaawn(join("node_modules", ".bin", bin + EXT), args, OPTS);
}

17
src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
/*
* 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
*/
declare global {
export var VencordDesktopNative: typeof import("preload/VencordDesktopNative").VencordDesktopNative;
export var VencordDesktop: typeof import("renderer/index");
// TODO
export var Vencord: any;
export var vcdLS: typeof localStorage;
export var IS_DEV: boolean;
}
export {};

25
src/main/about.ts Normal file
View File

@@ -0,0 +1,25 @@
/*
* 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 { BrowserWindow } from "electron";
import { join } from "path";
import { ICON_PATH, STATIC_DIR } from "shared/paths";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
export function createAboutWindow() {
const about = new BrowserWindow({
center: true,
autoHideMenuBar: true,
icon: ICON_PATH
});
makeLinksOpenExternally(about);
about.loadFile(join(STATIC_DIR, "about.html"));
return about;
}

View File

@@ -1,7 +1,26 @@
/*
* 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 { join } from "path";
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? join(app.getPath("userData"), "VencordDesktop");
export const VENCORD_FILES_DIR = join(DATA_DIR, "vencordDist");
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR || join(app.getPath("userData"), "VencordDesktop");
export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings");
export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css");
export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json");
// needs to be inline require because of circular dependency
// as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised
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)`;
// dimensions shamelessly stolen from Discord Desktop :3
export const MIN_WIDTH = 940;
export const MIN_HEIGHT = 500;
export const DEFAULT_WIDTH = 1280;
export const DEFAULT_HEIGHT = 720;

View File

@@ -1,20 +1,30 @@
import { app, BrowserWindow } from 'electron';
import { createMainWindow } from "./mainWindow";
import { createSplashWindow } from "./splash";
import { join } from "path";
import { DATA_DIR, VENCORD_FILES_DIR } from "./constants";
import { once } from "../shared/utils/once";
import { ensureVencordFiles } from "./utils/vencordLoader";
/*
* 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 "./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 { Settings } from "./settings";
import { createSplashWindow } from "./splash";
import { ensureVencordFiles } from "./utils/vencordLoader";
if (IS_DEV) {
require("source-map-support").install();
}
// Make the Vencord files use our DATA_DIR
process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "main.js")));
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
let mainWin: BrowserWindow | null = null;
@@ -25,14 +35,19 @@ if (!app.requestSingleInstanceLock()) {
app.on("second-instance", () => {
if (mainWin) {
if (mainWin.isMinimized()) mainWin.restore();
if (!mainWin.isVisible()) mainWin.show();
mainWin.focus();
}
});
app.whenReady().then(async () => {
checkUpdates();
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
else if (process.platform === "darwin") app.dock.setIcon(ICON_PATH);
createWindows();
app.on('activate', () => {
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindows();
});
});
@@ -49,10 +64,13 @@ async function createWindows() {
mainWin.once("ready-to-show", () => {
splash.destroy();
mainWin!.show();
if (Settings.store.maximized) {
mainWin!.maximize();
}
});
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin")
app.quit();
if (process.platform !== "darwin") app.quit();
});

View File

@@ -1,13 +1,98 @@
import { app, ipcMain } from "electron";
import { join } from "path";
import { GET_PRELOAD_FILE, RELAUNCH } from "../shared/IpcEvents";
import { VENCORD_FILES_DIR } from "./constants";
/*
* 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
*/
ipcMain.on(GET_PRELOAD_FILE, e => {
import { app, dialog, ipcMain, session, shell } from "electron";
import { existsSync, readFileSync, watch } from "fs";
import { open, readFile } from "fs/promises";
import { join } from "path";
import { debounce } from "shared/utils/debounce";
import { IpcEvents } from "../shared/IpcEvents";
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE } from "./constants";
import { mainWin } from "./mainWindow";
import { Settings } from "./settings";
ipcMain.on(IpcEvents.GET_VENCORD_PRELOAD_FILE, e => {
e.returnValue = join(VENCORD_FILES_DIR, "preload.js");
});
ipcMain.handle(RELAUNCH, () => {
ipcMain.on(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, e => {
e.returnValue = readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopRenderer.js"), "utf-8");
});
ipcMain.on(IpcEvents.GET_RENDERER_SCRIPT, e => {
e.returnValue = readFileSync(join(__dirname, "renderer.js"), "utf-8");
});
ipcMain.on(IpcEvents.GET_RENDERER_CSS_FILE, e => {
e.returnValue = join(__dirname, "renderer.css");
});
ipcMain.on(IpcEvents.GET_SETTINGS, e => {
e.returnValue = Settings.plain;
});
ipcMain.on(IpcEvents.GET_VERSION, e => {
e.returnValue = app.getVersion();
});
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: string) => {
Settings.setData(settings, path);
});
ipcMain.handle(IpcEvents.RELAUNCH, () => {
app.relaunch();
app.exit();
});
ipcMain.handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
shell.showItemInFolder(path);
});
ipcMain.handle(IpcEvents.FOCUS, e => {
e.sender.focus();
});
ipcMain.handle(IpcEvents.CLOSE, e => {
e.sender.close();
});
ipcMain.handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => {
const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 3);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
});
ipcMain.handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
const res = await dialog.showOpenDialog(mainWin!, {
properties: ["openDirectory"]
});
if (!res.filePaths.length) return "cancelled";
const dir = res.filePaths[0];
for (const file of ["vencordDesktopMain.js", "preload.js", "vencordDesktopRenderer.js", "renderer.css"]) {
if (!existsSync(join(dir, file))) return "invalid";
}
return dir;
});
function readCss() {
return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
}
open(VENCORD_QUICKCSS_FILE, "a+").then(fd => {
fd.close();
watch(
VENCORD_QUICKCSS_FILE,
{ persistent: false },
debounce(async () => {
mainWin?.webContents.postMessage("VencordQuickCssUpdate", await readCss());
}, 50)
);
});

View File

@@ -1,39 +1,27 @@
import { BrowserWindow, Menu, Tray, app, shell } from "electron";
/*
* 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, BrowserWindow, BrowserWindowConstructorOptions, Menu, Tray } from "electron";
import { join } from "path";
import { ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about";
import { DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH } from "./constants";
import { Settings, VencordSettings } from "./settings";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { downloadVencordFiles } from "./utils/vencordLoader";
let isQuitting = false;
let tray: Tray;
app.on("before-quit", () => {
isQuitting = true;
});
function initWindowOpenHandler(win: BrowserWindow) {
win.webContents.setWindowOpenHandler(({ url }) => {
switch (url) {
case "about:blank":
case "https://discord.com/popout":
return { action: "allow" };
}
try {
var protocol = new URL(url).protocol;
} catch {
return { action: "deny" };
}
switch (protocol) {
case "http:":
case "https:":
case "mailto:":
case "steam:":
case "spotify:":
shell.openExternal(url);
}
return { action: "deny" };
});
}
export let mainWin: BrowserWindow;
function initTray(win: BrowserWindow) {
const trayMenu = Menu.buildFromTemplate([
@@ -44,6 +32,28 @@ function initTray(win: BrowserWindow) {
},
enabled: false
},
{
label: "About",
click: createAboutWindow
},
{
label: "Update Vencord",
async click() {
await downloadVencordFiles();
app.relaunch();
app.quit();
}
},
{
type: "separator"
},
{
label: "Relaunch",
click() {
app.relaunch();
app.quit();
}
},
{
label: "Quit Vencord Desktop",
click() {
@@ -53,7 +63,7 @@ function initTray(win: BrowserWindow) {
}
]);
const tray = new Tray(ICON_PATH);
tray = new Tray(ICON_PATH);
tray.setToolTip("Vencord Desktop");
tray.setContextMenu(trayMenu);
tray.on("click", () => win.show());
@@ -67,8 +77,150 @@ function initTray(win: BrowserWindow) {
});
}
function initMenuBar(win: BrowserWindow) {
const isWindows = process.platform === "win32";
const wantCtrlQ = !isWindows || VencordSettings.store.winCtrlQ;
const menu = Menu.buildFromTemplate([
{
label: "Vencord Desktop",
role: "appMenu",
submenu: [
{
label: "About Vencord Desktop",
role: "about",
click: createAboutWindow
},
{
label: "Force Update Vencord",
async click() {
await downloadVencordFiles();
app.relaunch();
app.quit();
},
toolTip: "Vencord Desktop will automatically restart after this operation"
},
{
label: "Relaunch",
accelerator: "CmdOrCtrl+Shift+R",
click() {
app.relaunch();
app.quit();
}
},
{
label: "Quit",
accelerator: wantCtrlQ ? "CmdOrCtrl+Q" : void 0,
visible: !isWindows,
role: "quit",
click() {
app.quit();
}
},
{
label: "Quit",
accelerator: isWindows ? "Alt+F4" : void 0,
visible: isWindows,
role: "quit",
click() {
app.quit();
}
}
]
},
{ 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
}
]);
Menu.setApplicationMenu(menu);
}
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
const { x, y, width, height } = Settings.store.windowBounds ?? {};
const options = {
width: width ?? DEFAULT_WIDTH,
height: height ?? DEFAULT_HEIGHT
} as BrowserWindowConstructorOptions;
if (x != null && y != null) {
options.x = x;
options.y = y;
}
if (!Settings.store.disableMinSize) {
options.minWidth = MIN_WIDTH;
options.minHeight = MIN_HEIGHT;
}
return options;
}
function initWindowBoundsListeners(win: BrowserWindow) {
const saveState = () => {
Settings.store.maximized = win.isMaximized();
Settings.store.minimized = win.isMinimized();
};
win.on("maximize", saveState);
win.on("minimize", saveState);
win.on("unmaximize", saveState);
const saveBounds = () => {
Settings.store.windowBounds = win.getBounds();
};
win.on("resize", saveBounds);
win.on("move", saveBounds);
}
function initSettingsListeners(win: BrowserWindow) {
Settings.addChangeListener("tray", enable => {
if (enable) initTray(win);
else tray?.destroy();
});
Settings.addChangeListener("disableMinSize", disable => {
if (disable) {
// 0 no work
win.setMinimumSize(1, 1);
} else {
win.setMinimumSize(MIN_WIDTH, MIN_HEIGHT);
const { width, height } = win.getBounds();
win.setBounds({
width: Math.max(width, MIN_WIDTH),
height: Math.max(height, MIN_HEIGHT)
});
}
});
VencordSettings.addChangeListener("macosTranslucency", enabled => {
if (enabled) {
win.setVibrancy("sidebar");
win.setBackgroundColor("#ffffff00");
} else {
win.setVibrancy(null);
win.setBackgroundColor("#ffffff");
}
});
}
export function createMainWindow() {
const win = new BrowserWindow({
const win = (mainWin = new BrowserWindow({
show: false,
autoHideMenuBar: true,
webPreferences: {
@@ -78,11 +230,19 @@ export function createMainWindow() {
devTools: true,
preload: join(__dirname, "preload.js")
},
icon: ICON_PATH
});
icon: ICON_PATH,
frame: VencordSettings.store.frameless !== true,
...(VencordSettings.store.macosTranslucency
? {
vibrancy: "sidebar",
backgroundColor: "#ffffff00"
}
: {}),
...getWindowBoundsOptions()
}));
win.on("close", e => {
if (isQuitting) return;
if (isQuitting || Settings.store.minimizeToTray === false || Settings.store.tray === false) return;
e.preventDefault();
win.hide();
@@ -90,10 +250,18 @@ export function createMainWindow() {
return false;
});
initTray(win);
initWindowOpenHandler(win);
initWindowBoundsListeners(win);
if (Settings.store.tray ?? true) initTray(win);
initMenuBar(win);
makeLinksOpenExternally(win);
initSettingsListeners(win);
win.loadURL("https://discord.com/app");
const subdomain =
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"
? `${Settings.store.discordBranch}.`
: "";
win.loadURL(`https://${subdomain}discord.com/app`);
return win;
}

34
src/main/settings.ts Normal file
View File

@@ -0,0 +1,34 @@
/*
* 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 { readFileSync, writeFileSync } from "fs";
import { join } from "path";
import type { Settings as TSettings } from "shared/settings";
import { SettingsStore } from "shared/utils/SettingsStore";
import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants";
const SETTINGS_FILE = join(DATA_DIR, "settings.json");
function loadSettings<T extends object = any>(file: string, name: string) {
let settings = {} as T;
try {
const content = readFileSync(file, "utf8");
try {
settings = JSON.parse(content);
} catch (err) {
console.error(`Failed to parse ${name} settings.json:`, err);
}
} catch {}
const store = new SettingsStore(settings);
store.addGlobalChangeListener(o => writeFileSync(file, JSON.stringify(o, null, 4)));
return store;
}
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vencord Desktop");
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord");

View File

@@ -1,18 +1,18 @@
/*
* 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 { BrowserWindow } from "electron";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { STATIC_DIR } from "shared/paths";
export function createSplashWindow() {
const splash = new BrowserWindow({
transparent: true,
frame: false,
height: 350,
width: 300,
center: true,
resizable: false,
maximizable: false
});
const splash = new BrowserWindow(SplashProps);
splash.loadFile(join(__dirname, "..", "..", "static", "splash.html"));
splash.loadFile(join(STATIC_DIR, "splash.html"));
return splash;
}

View File

@@ -1,14 +1,22 @@
/*
* 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 { createWriteStream } from "fs";
import type { IncomingMessage } from "http";
import { RequestOptions, get } from "https";
import { get, RequestOptions } from "https";
import { finished } from "stream/promises";
export async function downloadFile(url: string, file: string, options: RequestOptions = {}) {
const res = await simpleReq(url, options);
await finished(
res.pipe(createWriteStream(file, {
autoClose: true
}))
res.pipe(
createWriteStream(file, {
autoClose: true
})
)
);
}
@@ -16,12 +24,8 @@ export function simpleReq(url: string, options: RequestOptions = {}) {
return new Promise<IncomingMessage>((resolve, reject) => {
get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400)
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300)
return simpleReq(headers.location!, options)
.then(resolve)
.catch(reject);
if (statusCode! >= 400) return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300) return simpleReq(headers.location!, options).then(resolve).catch(reject);
resolve(res);
});

View File

@@ -0,0 +1,40 @@
/*
* 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 { BrowserWindow, shell } from "electron";
import { Settings } from "../settings";
export function makeLinksOpenExternally(win: BrowserWindow) {
win.webContents.setWindowOpenHandler(({ url }) => {
switch (url) {
case "about:blank":
case "https://discord.com/popout":
return { action: "allow" };
}
try {
var { protocol } = new URL(url);
} catch {
return { action: "deny" };
}
switch (protocol) {
case "http:":
case "https:":
if (Settings.store.openLinksWithElectron) {
return { action: "allow" };
}
// eslint-disable-next-line no-fallthrough
case "mailto:":
case "steam:":
case "spotify:":
shell.openExternal(url);
}
return { action: "deny" };
});
}

View File

@@ -1,53 +1,57 @@
/*
* 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 { existsSync, mkdirSync } from "fs";
import type { RequestOptions } from "https";
import { join } from "path";
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { downloadFile, simpleGet } from "./http";
// TODO: Setting to switch repo
const API_BASE = "https://api.github.com/repos/Vendicated/Vencord";
const API_BASE = "https://api.github.com";
const FILES_TO_DOWNLOAD = [
"vencordDesktopMain.js",
"preload.js",
"vencordDesktopRenderer.js",
"renderer.css"
];
const FILES_TO_DOWNLOAD = ["vencordDesktopMain.js", "preload.js", "vencordDesktopRenderer.js", "renderer.css"];
export interface ReleaseData {
name: string;
tag_name: string;
html_url: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
}
export async function githubGet(endpoint: string) {
return simpleGet(API_BASE + endpoint, {
const opts: RequestOptions = {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": USER_AGENT
}
});
};
if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
return simpleGet(API_BASE + endpoint, opts);
}
export async function downloadVencordFiles() {
const release = await githubGet("/releases/latest");
const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
const data = JSON.parse(release.toString("utf-8"));
const assets = data.assets as Array<{
name: string;
browser_download_url: string;
}>;
const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData;
await Promise.all(
assets
.filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
.map(({ name, browser_download_url }) =>
downloadFile(
browser_download_url,
join(
VENCORD_FILES_DIR,
name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase())
)
)
)
.map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name)))
);
}
export async function ensureVencordFiles() {
if (existsSync(join(VENCORD_FILES_DIR, "main.js"))) return;
if (existsSync(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))) return;
mkdirSync(VENCORD_FILES_DIR, { recursive: true });
await downloadVencordFiles();

View File

@@ -1,9 +0,0 @@
import { ipcRenderer } from "electron";
import { RELAUNCH } from "../shared/IpcEvents";
export const VencordDesktop = {
app: {
relaunch: () => ipcRenderer.invoke(RELAUNCH)
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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 type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest";
import { IpcEvents } from "../shared/IpcEvents";
import { invoke, sendSync } from "./typedIpcs";
export const VencordDesktopNative = {
app: {
relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION)
},
fileManager: {
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
selectVencordDir: () => invoke<LiteralUnion<"cancelled" | "invalid", string>>(IpcEvents.SELECT_VENCORD_DIR)
},
settings: {
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
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
},
win: {
focus: () => invoke<void>(IpcEvents.FOCUS)
}
};

View File

@@ -1,7 +1,44 @@
import { contextBridge, ipcRenderer } from "electron";
import { GET_PRELOAD_FILE } from "../shared/IpcEvents";
import { VencordDesktop } from "./VencordDesktop";
/*
* 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
*/
contextBridge.exposeInMainWorld("VencordDesktop", VencordDesktop);
import { contextBridge, ipcRenderer, webFrame } from "electron";
import { readFileSync, watch } from "fs";
require(ipcRenderer.sendSync(GET_PRELOAD_FILE));
import { IpcEvents } from "../shared/IpcEvents";
import { VencordDesktopNative } from "./VencordDesktopNative";
contextBridge.exposeInMainWorld("VencordDesktopNative", VencordDesktopNative);
require(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_PRELOAD_FILE));
webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT));
webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_RENDERER_SCRIPT));
// #region css
const rendererCss = ipcRenderer.sendSync(IpcEvents.GET_RENDERER_CSS_FILE);
const style = document.createElement("style");
style.id = "vcd-css-core";
style.textContent = readFileSync(rendererCss, "utf-8");
if (document.readyState === "complete") {
document.documentElement.appendChild(style);
} else {
document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), {
once: true
});
}
if (IS_DEV) {
// persistent means keep process running if watcher is the only thing still running
// which we obviously don't want
watch(rendererCss, { persistent: false }, () => {
document.getElementById("vcd-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
});
}
// #endregion
VencordDesktopNative.spellcheck.setLanguages(window.navigator.languages);

16
src/preload/typedIpcs.ts Normal file
View File

@@ -0,0 +1,16 @@
/*
* 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 { ipcRenderer } from "electron";
import { IpcEvents } from "shared/IpcEvents";
export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
}
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}

View File

@@ -0,0 +1,122 @@
/*
* 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 "./settings.css";
import { useSettings } from "renderer/settings";
import { Common, Util } from "../vencord";
const { Margins } = Util;
export default function SettingsUi() {
const Settings = useSettings();
const {
Forms: { FormSection, FormText, FormDivider, FormSwitch, FormTitle },
Text,
Select,
Button
} = Common;
const switches: [keyof typeof Settings, string, string, boolean?, (() => boolean)?][] = [
["tray", "Tray Icon", "Add a tray icon for Vencord Desktop", true],
[
"minimizeToTray",
"Minimize to tray",
"Hitting X will make Vencord Desktop minimize to the tray instead of closing",
true,
() => Settings.tray ?? true
],
[
"disableMinSize",
"Disable minimum window size",
"Allows you to make the window as small as your heart desires"
],
[
"openLinksWithElectron",
"Open Links in app (experimental)",
"Opens links in a new Vencord Desktop window instead of your web browser"
]
];
return (
<FormSection>
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">
Vencord Desktop Settings
</Text>
<FormTitle className={Margins.top16}>Discord Branch</FormTitle>
<Select
placeholder="Stable"
options={[
{ label: "Stable", value: "stable", default: true },
{ label: "Canary", value: "canary" },
{ label: "PTB", value: "ptb" }
]}
closeOnSelect={true}
select={v => (Settings.discordBranch = v)}
isSelected={v => v === Settings.discordBranch}
serialize={s => s}
/>
<FormDivider className={Margins.top16 + " " + Margins.bottom16} />
{switches.map(([key, text, note, def, predicate]) => (
<FormSwitch
value={(Settings[key] ?? def ?? false) && (!predicate || predicate())}
disabled={predicate && !predicate()}
onChange={v => (Settings[key] = v)}
note={note}
key={key}
>
{text}
</FormSwitch>
))}
<FormTitle>Vencord Location</FormTitle>
<FormText>
Vencord files are loaded from{" "}
{Settings.vencordDir ? (
<a
href="about:blank"
onClick={e => {
e.preventDefault();
VencordDesktopNative.fileManager.showItemInFolder(Settings.vencordDir!);
}}
>
{Settings.vencordDir}
</a>
) : (
"the default location"
)}
</FormText>
<div className="vcd-location-btns">
<Button
size={Button.Sizes.SMALL}
onClick={async () => {
const choice = await VencordDesktopNative.fileManager.selectVencordDir();
switch (choice) {
case "cancelled":
case "invalid":
// TODO
return;
}
Settings.vencordDir = choice;
}}
>
Change
</Button>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
onClick={() => (Settings.vencordDir = void 0)}
>
Reset
</Button>
</div>
</FormSection>
);
}

View File

@@ -0,0 +1,7 @@
/*
* 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
*/
export { default as Settings } from "./Settings";

View File

@@ -0,0 +1,6 @@
.vcd-location-btns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5em;
margin-top: 0.5em;
}

31
src/renderer/fixes.ts Normal file
View File

@@ -0,0 +1,31 @@
/*
* 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 "./hideGarbage.css";
import { isFirstRun, localStorage } from "./utils";
// Make clicking Notifications focus the window
const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!;
Object.defineProperty(Notification.prototype, "onclick", {
set(onClick) {
originalSetOnClick.call(this, function (this: unknown) {
onClick.apply(this, arguments);
VencordDesktopNative.win.focus();
});
},
configurable: true
});
// Enable Desktop Notifications by default
if (isFirstRun) {
// Hide "Download Discord Desktop now!!!!" banner
localStorage.setItem("hideNag", "true");
Vencord.Webpack.waitFor("setDesktopType", m => {
m.setDesktopType("all");
});
}

View File

@@ -0,0 +1,5 @@
/* Download Desktop button in guilds list */
[class|=listItem]:has([data-list-item-id=guildsnav___app-download-button]),
[class|=listItem]:has(+ [class|=listItem] [data-list-item-id=guildsnav___app-download-button]) {
display: none;
}

12
src/renderer/index.ts Normal file
View File

@@ -0,0 +1,12 @@
/*
* 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 "./fixes";
console.log("read if cute :3");
export * as Components from "./components";
export { Settings } from "./settings";

31
src/renderer/settings.ts Normal file
View File

@@ -0,0 +1,31 @@
/*
* 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 { SettingsStore } from "shared/utils/SettingsStore";
import { Common } from "./vencord";
export const Settings = new SettingsStore(VencordDesktopNative.settings.get());
Settings.addGlobalChangeListener((o, p) => VencordDesktopNative.settings.set(o, p));
export function useSettings() {
const [, update] = Common.React.useReducer(x => x + 1, 0);
Common.React.useEffect(() => {
Settings.addGlobalChangeListener(update);
return () => Settings.removeGlobalChangeListener(update);
}, []);
return Settings.store;
}
export function getValueAndOnChange(key: keyof typeof Settings.store) {
return {
value: Settings.store[key] as any,
onChange: (value: any) => (Settings.store[key] = value)
};
}

14
src/renderer/utils.ts Normal file
View File

@@ -0,0 +1,14 @@
/*
* 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
*/
export const localStorage = (window.vcdLS = window.localStorage);
export const isFirstRun = (() => {
const key = "VCD_FIRST_RUN";
if (localStorage.getItem(key) !== null) return false;
localStorage.setItem(key, "false");
return true;
})();

13
src/renderer/vencord.ts Normal file
View File

@@ -0,0 +1,13 @@
/*
* 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
*/
// FIXME: this is terrible
const { Webpack, Plugins, Util } = Vencord;
const { Common } = Webpack;
const { plugins } = Plugins;
export { Common, Plugins, plugins, Util, Webpack };

View File

@@ -1,2 +1,31 @@
export const GET_PRELOAD_FILE = "VCD_GET_PRELOAD_FILE";
export const RELAUNCH = "VCD_RELAUNCH";
/*
* 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
*/
export const enum IpcEvents {
GET_VENCORD_PRELOAD_FILE = "VCD_GET_VC_PRELOAD_FILE",
GET_VENCORD_RENDERER_SCRIPT = "VCD_GET_VC_RENDERER_SCRIPT",
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",
SHOW_ITEM_IN_FOLDER = "VCD_SHOW_ITEM_IN_FOLDER",
GET_SETTINGS = "VCD_GET_SETTINGS",
SET_SETTINGS = "VCD_SET_SETTINGS",
SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",
UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA",
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES",
CLOSE = "VCD_CLOSE"
}

View File

@@ -0,0 +1,18 @@
/*
* 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 type { BrowserWindowConstructorOptions } from "electron";
export const SplashProps: BrowserWindowConstructorOptions = {
transparent: true,
frame: false,
height: 350,
width: 300,
center: true,
resizable: false,
maximizable: false,
alwaysOnTop: true
};

View File

@@ -1,4 +1,10 @@
/*
* 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 { join } from "path";
export const STATIC_DIR = join(__dirname, "..", "..", "static");
export const ICON_PATH = join(STATIC_DIR, "icon.ico");
export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png");

20
src/shared/settings.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
/*
* 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 type { Rectangle } from "electron";
export interface Settings {
maximized?: boolean;
minimized?: boolean;
windowBounds?: Rectangle;
discordBranch?: "stable" | "canary" | "ptb";
openLinksWithElectron?: boolean;
vencordDir?: string;
disableMinSize?: boolean;
tray?: boolean;
minimizeToTray?: boolean;
skippedUpdate?: string;
}

View File

@@ -0,0 +1,147 @@
/*
* 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 { LiteralUnion } from "type-fest";
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
? Pre extends keyof T
? ResolvePropDeep<T[Pre], Suf>
: any
: P extends keyof T
? T[P]
: any;
/**
* The SettingsStore allows you to easily create a mutable store that
* has support for global and path-based change listeners.
*/
export class SettingsStore<T extends object> {
private pathListeners = new Map<string, Set<(newData: any) => void>>();
private globalListeners = new Set<(newData: T, path: string) => void>();
/**
* The store object. Making changes to this object will trigger the applicable change listeners
*/
public declare store: T;
/**
* The plain data. Changes to this object will not trigger any change listeners
*/
public declare plain: T;
public constructor(plain: T) {
this.plain = plain;
this.store = this.makeProxy(plain);
}
private makeProxy(object: any, root: T = object, path: string = "") {
const self = this;
return new Proxy(object, {
get(target, key: string) {
const v = target[key];
if (typeof v === "object" && v !== null && !Array.isArray(v))
return self.makeProxy(v, root, `${path}${path && "."}${key}`);
return v;
},
set(target, key: string, value) {
if (target[key] === value) return true;
Reflect.set(target, key, value);
const setPath = `${path}${path && "."}${key}`;
self.globalListeners.forEach(cb => cb(root, setPath));
self.pathListeners.get(setPath)?.forEach(cb => cb(value));
return true;
}
});
}
/**
* Set the data of the store.
* This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables)
*
* Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data
* @param value New data
* @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc
*/
public setData(value: T, pathToNotify?: string) {
this.plain = value;
this.store = this.makeProxy(value);
if (pathToNotify) {
let v = value;
const path = pathToNotify.split(".");
for (const p of path) {
if (!v) {
console.warn(
`Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update`
);
return;
}
v = v[p];
}
this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v));
}
this.globalListeners.forEach(cb => cb(value, ""));
}
/**
* Add a global change listener, that will fire whenever any setting is changed
*/
public addGlobalChangeListener(cb: (data: T, path: string) => void) {
this.globalListeners.add(cb);
}
/**
* Add a scoped change listener that will fire whenever a setting matching the specified path is changed.
*
* For example if path is `"foo.bar"`, the listener will fire on
* ```js
* Setting.store.foo.bar = "hi"
* ```
* but not on
* ```js
* Setting.store.foo.baz = "hi"
* ```
* @param path
* @param cb
*/
public addChangeListener<P extends LiteralUnion<keyof T, string>>(
path: P,
cb: (data: ResolvePropDeep<T, P>) => void
) {
const listeners = this.pathListeners.get(path as string) ?? new Set();
listeners.add(cb);
this.pathListeners.set(path as string, listeners);
}
/**
* Remove a global listener
* @see {@link addGlobalChangeListener}
*/
public removeGlobalChangeListener(cb: (data: T, path: string) => void) {
this.globalListeners.delete(cb);
}
/**
* Remove a scoped listener
* @see {@link addChangeListener}
*/
public removeChangeListener(path: LiteralUnion<keyof T, string>, cb: (data: any) => void) {
const listeners = this.pathListeners.get(path as string);
if (!listeners) return;
listeners.delete(cb);
if (!listeners.size) this.pathListeners.delete(path as string);
}
}

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
*/
/**
* Returns a new function that will only be called after the given delay.
* Subsequent calls will cancel the previous timeout and start a new one from 0
*
* Useful for grouping multiple calls into one
*/
export function debounce<T extends Function>(func: T, delay = 300): T {
let timeout: NodeJS.Timeout;
return function (...args: any[]) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func(...args);
}, delay);
} as any;
}

View File

@@ -0,0 +1,23 @@
/*
* 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

@@ -1,3 +1,14 @@
/*
* 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
*/
/**
* Wraps the given function so that it can only be called once
* @param fn Function to wrap
* @returns New function that can only be called once
*/
export function once<T extends Function>(fn: T): T {
let called = false;
return function (this: any, ...args: any[]) {

98
src/updater/main.ts Normal file
View File

@@ -0,0 +1,98 @@
/*
* 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, BrowserWindow, ipcMain, shell } from "electron";
import { Settings } from "main/settings";
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";
export interface UpdateData {
currentVersion: string;
latestVersion: string;
release: ReleaseData;
}
let updateData: UpdateData;
ipcMain.handle(IpcEvents.UPDATER_GET_DATA, () => updateData);
ipcMain.handle(IpcEvents.UPDATER_DOWNLOAD, () => {
const { assets } = updateData.release;
const url = (() => {
switch (process.platform) {
case "win32":
return assets.find(a => a.name.endsWith(".exe"))!.browser_download_url;
case "darwin":
return assets.find(a => a.name.endsWith(".dmg"))!.browser_download_url;
case "linux":
return updateData.release.html_url;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
})();
shell.openExternal(url);
});
ipcMain.handle(IpcEvents.UPDATE_IGNORE, () => {
Settings.store.skippedUpdate = updateData.latestVersion;
});
function isOutdated(oldVersion: string, newVersion: string) {
const oldParts = oldVersion.split(".");
const newParts = newVersion.split(".");
if (oldParts.length !== newParts.length)
throw new Error(`Incompatible version strings (old: ${oldVersion}, new: ${newVersion})`);
for (let i = 0; i < oldParts.length; i++) {
const oldPart = Number(oldParts[i]);
const newPart = Number(newParts[i]);
if (isNaN(oldPart) || isNaN(newPart))
throw new Error(`Invalid version string (old: ${oldVersion}, new: ${newVersion})`);
if (oldPart < newPart) return true;
if (oldPart > newPart) return false;
}
return false;
}
export async function checkUpdates() {
// if (IS_DEV) return;
try {
const raw = await githubGet("/repos/Vencord/Desktop/releases/latest");
const data = JSON.parse(raw.toString("utf-8")) as ReleaseData;
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
};
openNewUpdateWindow();
}
} catch (e) {
console.error("AppUpdater: Failed to check for updates\n", e);
}
}
function openNewUpdateWindow() {
const win = new BrowserWindow({
...SplashProps,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
}
});
win.loadFile(join(STATIC_DIR, "updater.html"));
}

21
src/updater/preload.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 { contextBridge } from "electron";
import { invoke } from "preload/typedIpcs";
import { IpcEvents } from "shared/IpcEvents";
import type { UpdateData } from "./main";
contextBridge.exposeInMainWorld("Updater", {
getData: () => invoke<UpdateData>(IpcEvents.UPDATER_GET_DATA),
download: () => {
invoke<void>(IpcEvents.UPDATER_DOWNLOAD);
invoke<void>(IpcEvents.CLOSE);
},
ignore: () => invoke<void>(IpcEvents.UPDATE_IGNORE),
close: () => invoke<void>(IpcEvents.CLOSE)
});

34
static/about.html Normal file
View File

@@ -0,0 +1,34 @@
<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/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1,29 +1,48 @@
<head>
<style>
body {
margin: 0;
padding: 0;
}
<style>
* {
user-select: none;
}
.wrapper {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: hsl(223 6.7% 20.6%);
border-radius: 6px;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
margin: 0;
padding: 0;
}
p {
color: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
}
</style>
.wrapper {
box-sizing: border-box;
height: 100%;
display: flex;
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%);
}
p {
color: rgb(219, 222, 225);
text-align: center;
}
img {
width: 6em;
height: 6em;
}
</style>
</head>
<body>
<div class="wrapper">
<p>Loading...</p>
</div>
<div class="wrapper">
<img
draggable="false"
src="https://cdn.discordapp.com/emojis/1024751291504791654.gif?size=512"
alt="shiggy"
role="presentation"
/>
<p>Loading Vencord Desktop...</p>
</div>
</body>

115
static/updater.html Normal file
View File

@@ -0,0 +1,115 @@
<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);
}
.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;
padding: 1em;
}
h1 {
text-align: center;
}
.buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5em;
margin-top: 0.25em;
}
button {
cursor: pointer;
padding: 0.5em;
color: white;
border: none;
border-radius: 3px;
font-weight: bold;
transition: filter 0.2 ease-in-out;
}
button:hover,
button:active {
filter: brightness(0.9);
}
.green {
background-color: #248046;
}
.red {
background-color: #ed4245;
}
</style>
</head>
<body>
<div class="wrapper">
<section>
<h1>Update Available</h1>
<p>There's a new update for Vencord Desktop! Update now to get new fixes and features!</p>
<p>
Current: <span id="current"></span>
<br />
Latest: <span id="latest"></span>
</p>
</section>
<section>
<label id="disable-remind">
<input type="checkbox" />
<span>Do not remind again for </span>
</label>
<div class="buttons">
<button name="download" class="green">Download Update</button>
<button name="close" class="red">Close</button>
</div>
</section>
</div>
</body>
<script type="module">
const data = await Updater.getData();
document.getElementById("current").textContent = data.currentVersion;
document.getElementById("latest").textContent = data.latestVersion;
document.querySelector("#disable-remind > span").textContent += data.latestVersion;
function checkDisableRemind() {
const checkbox = document.querySelector("#disable-remind > input");
if (checkbox.checked) {
Updater.ignore();
}
}
const onClicks = {
download() {
checkDisableRemind();
Updater.download();
},
close() {
checkDisableRemind();
Updater.close();
}
};
for (const name in onClicks) {
document.querySelectorAll(`button[name="${name}"]`).forEach(button => {
button.addEventListener("click", onClicks[name]);
});
}
</script>

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"lib": ["DOM", "DOM.Iterable", "esnext", "esnext.array", "esnext.asynciterable", "esnext.symbol"],
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"noImplicitAny": false,
"target": "ESNEXT",
"jsx": "preserve",
"baseUrl": "./src/"
},
"include": ["src/**/*"]
}