├── .env.example
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── blank.yml
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature-request.yml
└── workflows
│ ├── meta.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .npmrc
├── .prettierrc.yaml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── build
├── background.tiff
├── entitlements.mac.plist
├── icon.icns
├── icon.png
└── installer.nsh
├── meta
└── io.github.verticalsync.sunroof.metainfo.xml
├── package.json
├── patches
└── arrpc@3.4.0.patch
├── pnpm-lock.yaml
├── scripts
├── build
│ ├── build.mts
│ ├── injectReact.mjs
│ ├── sandboxFix.js
│ └── vencordDep.mts
├── header.txt
├── start.ts
├── startWatch.mts
└── utils
│ ├── dotenv.ts
│ ├── spawn.mts
│ └── updateMeta.mts
├── src
├── globals.d.ts
├── main
│ ├── about.ts
│ ├── appBadge.ts
│ ├── arrpc.ts
│ ├── autoStart.ts
│ ├── constants.ts
│ ├── firstLaunch.ts
│ ├── index.ts
│ ├── ipc.ts
│ ├── mainWindow.ts
│ ├── mediaPermissions.ts
│ ├── screenShare.ts
│ ├── settings.ts
│ ├── splash.ts
│ ├── utils
│ │ ├── http.ts
│ │ ├── ipcWrappers.ts
│ │ ├── makeLinksOpenExternally.ts
│ │ ├── popout.ts
│ │ ├── steamOS.ts
│ │ └── vencordLoader.ts
│ └── venmic.ts
├── preload
│ ├── VesktopNative.ts
│ ├── index.ts
│ └── typedIpc.ts
├── renderer
│ ├── appBadge.ts
│ ├── components
│ │ ├── ScreenSharePicker.tsx
│ │ ├── index.ts
│ │ ├── screenSharePicker.css
│ │ └── settings
│ │ │ ├── AutoStartToggle.tsx
│ │ │ ├── CustomSplashAnimation.tsx
│ │ │ ├── DiscordBranchPicker.tsx
│ │ │ ├── NotificationBadgeToggle.tsx
│ │ │ ├── Settings.tsx
│ │ │ ├── VencordLocationPicker.tsx
│ │ │ ├── WindowsTransparencyControls.tsx
│ │ │ └── settings.css
│ ├── fixes.css
│ ├── fixes.ts
│ ├── index.ts
│ ├── patches
│ │ ├── enableNotificationsByDefault.ts
│ │ ├── hideSwitchDevice.tsx
│ │ ├── hideVenmicInput.tsx
│ │ ├── index.ts
│ │ ├── platformClass.tsx
│ │ ├── screenShareFixes.ts
│ │ ├── shared.ts
│ │ ├── spellCheck.tsx
│ │ └── windowsTitleBar.tsx
│ ├── settings.ts
│ ├── themedSplash.ts
│ └── utils.ts
└── shared
│ ├── IpcEvents.ts
│ ├── browserWinProperties.ts
│ ├── paths.ts
│ ├── settings.d.ts
│ └── utils
│ ├── SettingsStore.ts
│ ├── debounce.ts
│ ├── guards.ts
│ ├── once.ts
│ └── sleep.ts
├── static
├── badges
│ ├── 1.ico
│ ├── 10.ico
│ ├── 11.ico
│ ├── 2.ico
│ ├── 3.ico
│ ├── 4.ico
│ ├── 5.ico
│ ├── 6.ico
│ ├── 7.ico
│ ├── 8.ico
│ └── 9.ico
├── dist
│ └── .gitignore
├── icon.ico
├── icon.png
├── troll.gif
└── views
│ ├── about.html
│ ├── first-launch.html
│ ├── splash.html
│ ├── style.css
│ └── updater.html
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # githubs api has a rate limit of 60/h if not authorised.
2 | # you may quickly hit that and get rate limited. To counteract this, you can provide a github token
3 | # here and it will be used. To do so, create a token at the following links and just leave
4 | # all permissions at the defaults (public repos read only, 0 permissions):
5 | # https://github.com/settings/personal-access-tokens/new
6 | GITHUB_TOKEN=
7 |
8 | ELECTRON_LAUNCH_FLAGS="--ozone-platform-hint=auto --enable-webrtc-pipewire-capturer --enable-features=WaylandWindowDecorations"
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "ignorePatterns": ["dist", "node_modules"],
5 | "plugins": [
6 | "@typescript-eslint",
7 | "license-header",
8 | "simple-import-sort",
9 | "unused-imports",
10 | "path-alias",
11 | "prettier"
12 | ],
13 | "settings": {
14 | "import/resolver": {
15 | "alias": {
16 | "map": []
17 | }
18 | }
19 | },
20 | "rules": {
21 | "license-header/header": ["error", "scripts/header.txt"],
22 | "eqeqeq": ["error", "always", { "null": "ignore" }],
23 | "spaced-comment": ["error", "always", { "markers": ["!"] }],
24 | "yoda": "error",
25 | "prefer-destructuring": [
26 | "error",
27 | {
28 | "VariableDeclarator": { "array": false, "object": true },
29 | "AssignmentExpression": { "array": false, "object": false }
30 | }
31 | ],
32 | "operator-assignment": ["error", "always"],
33 | "no-useless-computed-key": "error",
34 | "no-unneeded-ternary": ["error", { "defaultAssignment": false }],
35 | "no-invalid-regexp": "error",
36 | "no-constant-condition": ["error", { "checkLoops": false }],
37 | "no-duplicate-imports": "error",
38 | "no-extra-semi": "error",
39 | "dot-notation": "error",
40 | "no-useless-escape": "error",
41 | "no-fallthrough": "error",
42 | "for-direction": "error",
43 | "no-async-promise-executor": "error",
44 | "no-cond-assign": "error",
45 | "no-dupe-else-if": "error",
46 | "no-duplicate-case": "error",
47 | "no-irregular-whitespace": "error",
48 | "no-loss-of-precision": "error",
49 | "no-misleading-character-class": "error",
50 | "no-prototype-builtins": "error",
51 | "no-regex-spaces": "error",
52 | "no-shadow-restricted-names": "error",
53 | "no-unexpected-multiline": "error",
54 | "no-unsafe-optional-chaining": "error",
55 | "no-useless-backreference": "error",
56 | "use-isnan": "error",
57 | "prefer-const": "error",
58 | "prefer-spread": "error",
59 |
60 | "simple-import-sort/imports": "error",
61 | "simple-import-sort/exports": "error",
62 |
63 | "unused-imports/no-unused-imports": "error",
64 |
65 | "path-alias/no-relative": "error",
66 |
67 | "prettier/prettier": "error"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/blank.yml:
--------------------------------------------------------------------------------
1 | name: Blank Issue
2 | description: Reserved for developers. Use the bug report or feature request templates instead.
3 |
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | # READ THIS BEFORE OPENING AN ISSUE
9 |
10 | This form is only meant for Vesktop/Sunroof developers. If you don't know what you're doing,
11 | please use the bug report or feature request templates instead.
12 |
13 | - type: textarea
14 | id: content
15 | attributes:
16 | label: Content
17 | validations:
18 | required: true
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug / Crash Report
2 | description: Create a bug or crash report for Sunroof
3 | labels: [bug]
4 | title: "[Bug]
"
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | **Thanks 🩷 for taking the time to fill out this bug report! Before proceeding, please read the following**
11 |
12 | Make sure a similar issue doesn't already exist by [searching the existing issues](https://github.com/verticalsync/Sunroof/issues?q=is%3Aissue) for keywords!
13 |
14 | Make sure both Sunroof and Suncord are fully up to date. You can update Suncord by right-clicking the Sunroof tray icon and pressing "Update Suncord"
15 |
16 | **DO NOT REPORT** any of the following issues:
17 | - Purely graphical glitches like flickering, scaling issues, etc: Issue with your gpu. Nothing we can do, update drivers or disable hardware acceleration
18 | - Vencord related issues: This is the Vesktop repo, not Vencord
19 | - **SCREENSHARE NOT STARTING** / black screening on Linux: Issue with your desktop environment, specifically its xdg-desktop-portal.
20 | If that also doesn't work, you have to fix your systen. Inspect errors and google around.
21 |
22 | Linux users: Please only report issues with officially published packages
23 |
24 | - type: input
25 | id: discord
26 | attributes:
27 | label: Discord Account
28 | description: Who on Discord is making this request? Not required but encouraged for easier follow-up
29 | placeholder: username#0000
30 | validations:
31 | required: false
32 |
33 | - type: input
34 | id: os
35 | attributes:
36 | label: Operating System
37 | description: What operating system are you using (eg Windows 10, macOS Big Sur, Ubuntu 20.04)?
38 | placeholder: Windows 10
39 | validations:
40 | required: true
41 |
42 | - type: input
43 | id: linux-de
44 | attributes:
45 | label: Linux Only ~ Desktop Environment
46 | description: If you are on Linux, what Desktop environment are you using (eg GNOME, KDE, XFCE)? Are you using Wayland or Xorg?
47 | placeholder: Gnome on Wayland
48 | validations:
49 | required: false
50 |
51 | - type: textarea
52 | id: bug-description
53 | attributes:
54 | label: What happens when the bug or crash occurs?
55 | description: Where does this bug or crash occur, when does it occur, etc.
56 | placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
57 | validations:
58 | required: true
59 |
60 | - type: textarea
61 | id: expected-behaviour
62 | attributes:
63 | label: What is the expected behaviour?
64 | description: Simply detail what the expected behaviour is.
65 | placeholder: I expect Suncord/Discord to open the ... page instead of ..., it prevents me from doing ...
66 | validations:
67 | required: true
68 |
69 | - type: textarea
70 | id: steps-to-take
71 | attributes:
72 | label: How do you recreate this bug or crash?
73 | description: Give us a list of steps in order to recreate the bug or crash.
74 | placeholder: |
75 | 1. Do ...
76 | 2. Then ...
77 | 3. Do this ..., ... and then ...
78 | 4. Observe "the bug" or "the crash"
79 | validations:
80 | required: true
81 |
82 | - type: textarea
83 | id: debug-logs
84 | attributes:
85 | label: Debug Logs
86 | description: Run sunroof from the command line. Include the relevant command line output here
87 | value: |
88 | ```
89 | Replace this text with your crash-log. Do not remove the backticks
90 | ```
91 | validations:
92 | required: true
93 |
94 | - type: checkboxes
95 | id: agreement-check
96 | attributes:
97 | label: Request Agreement
98 | description: We only accept reports for bugs that happen on supported and up to date Sunroof releases
99 | options:
100 | - label: I have searched the existing issues and found no similar issue
101 | required: true
102 | - label: I am using the latest Sunroof and Suncord versions
103 | required: true
104 | - label: This issue occurs on an official release (not just the AUR packages)
105 | required: true
106 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Suncord Support Server
4 | url: https://discord.gg/VasF3Ma4Ab
5 | about: If you need help regarding Sunroof or Suncord, please join our support server!
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: 🛠️ Feature Request
2 | description: Request a feature for Sunroof
3 | labels: [enhancement]
4 | title: "[Feature Request] "
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | **Thanks 🩷 for taking the time to fill out this request! Before proceeding, please read the following**
11 |
12 | Make sure a similar request doesn't already exist by [searching the existing issues](https://github.com/verticalsync/Sunroof/issues?q=is%3Aissue) for keywords!
13 |
14 | This form is only meant for **Sunroof feature requests**.
15 | For plugin requests or Vencord feature requests, suggest them on the discord instead: https://discord.gg/VasF3Ma4Ab
16 |
17 | - type: input
18 | id: discord
19 | attributes:
20 | label: Discord Account
21 | description: Who on Discord is making this request? Not required but encouraged for easier follow-up
22 | placeholder: username#0000
23 | validations:
24 | required: false
25 |
26 | - type: textarea
27 | id: motivation
28 | attributes:
29 | label: Motivation
30 | description: If your feature request related to a problem? Please describe
31 | placeholder: I'm always frustrated when ..., I think it would be better if ...
32 | validations:
33 | required: true
34 |
35 | - type: textarea
36 | id: solution
37 | attributes:
38 | label: Solution
39 | description: Describe the solution you'd like
40 | placeholder: A clear and concise description of what you want to happen.
41 | validations:
42 | required: true
43 |
44 | - type: textarea
45 | id: alternatives
46 | attributes:
47 | label: Alternatives
48 | description: Describe alternatives you've considered
49 | placeholder: A clear and concise description of any alternative solutions or features you've considered.
50 | validations:
51 | required: true
52 |
53 | - type: textarea
54 | id: additional-context
55 | attributes:
56 | label: Additional context
57 | description: Add any other context here. Screenshots or mockups could help greatly
58 | validations:
59 | required: false
60 |
61 | - type: checkboxes
62 | id: agreement-check
63 | attributes:
64 | label: Request Agreement
65 | description: This form is only for Sunroof feature requests. If the following don't apply, re-read the introduction text
66 | options:
67 | - label: I have searched the existing issues and found no similar issue
68 | required: true
69 | - label: This is not a plugin request
70 | required: true
71 | - label: This is not a Suncord feature request
72 | required: true
73 |
--------------------------------------------------------------------------------
/.github/workflows/meta.yml:
--------------------------------------------------------------------------------
1 | name: Update metainfo on release
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 | workflow_dispatch:
8 |
9 | jobs:
10 | update:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: pnpm/action-setup@v4
16 |
17 | - name: Use Node.js 20
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 20
21 |
22 | - name: Install dependencies
23 | run: pnpm i
24 |
25 | - name: Update metainfo
26 | run: pnpm updateMeta
27 |
28 | - name: Commit and merge in changes
29 | run: |
30 | git config user.name "github-actions[bot]"
31 | git config user.email "60797172+github-actions[bot]@users.noreply.github.com"
32 | git checkout -b ci/meta-update
33 | git add meta/io.github.verticalsync.sunroof.metainfo.xml
34 | git commit -m "Insert release changes for ${{ github.event.release.tag_name }}"
35 | git push origin ci/meta-update --force
36 | gh pr create -B main -H ci/meta-update -t "Metainfo for ${{ github.event.release.tag_name }}" -b "This PR updates the metainfo for release ${{ github.event.release.tag_name }}. @verticalsync"
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 | workflow_dispatch:
8 |
9 | jobs:
10 | release:
11 | runs-on: ${{ matrix.os }}
12 |
13 | strategy:
14 | matrix:
15 | os: [macos-latest, ubuntu-latest, windows-latest]
16 | include:
17 | - os: macos-latest
18 | platform: mac
19 | - os: ubuntu-latest
20 | platform: linux
21 | - os: windows-latest
22 | platform: windows
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 | - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
27 |
28 | - name: Use Node.js 20
29 | uses: actions/setup-node@v4
30 | with:
31 | node-version: 20
32 | cache: "pnpm"
33 |
34 | - name: Install dependencies
35 | run: pnpm install --frozen-lockfile
36 |
37 | - name: Build
38 | run: pnpm build
39 |
40 | - name: Run Electron Builder
41 | if: ${{ matrix.platform != 'mac' }}
42 | run: |
43 | pnpm electron-builder --${{ matrix.platform }} --publish always
44 | env:
45 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
47 | - name: Run Electron Builder (mac)
48 | if: ${{ matrix.platform == 'mac' }}
49 | run: |
50 | pnpm electron-builder --${{ matrix.platform }} --publish always
51 | env:
52 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
16 |
17 | - name: Use Node.js 20
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 20
21 | cache: "pnpm"
22 |
23 | - name: Install dependencies
24 | run: pnpm install --frozen-lockfile
25 |
26 | - name: Run tests
27 | run: pnpm test
28 |
29 | - name: Test if it compiles
30 | run: |
31 | pnpm build
32 | pnpm build --dev
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .env
4 | .DS_Store
5 | .idea/
6 | .pnpm-store/
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
2 | package-manager-strict=false
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | tabWidth: 4
2 | semi: true
3 | printWidth: 120
4 | trailingComma: none
5 | bracketSpacing: true
6 | arrowParens: avoid
7 | useTabs: false
8 | endOfLine: lf
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": "explicit"
5 | },
6 | "[typescript]": {
7 | "editor.defaultFormatter": "esbenp.prettier-vscode"
8 | },
9 | "[javascript]": {
10 | "editor.defaultFormatter": "esbenp.prettier-vscode"
11 | },
12 | "[typescriptreact]": {
13 | "editor.defaultFormatter": "esbenp.prettier-vscode"
14 | },
15 | "[javascriptreact]": {
16 | "editor.defaultFormatter": "esbenp.prettier-vscode"
17 | },
18 | "[json]": {
19 | "editor.defaultFormatter": "esbenp.prettier-vscode"
20 | },
21 | "[jsonc]": {
22 | "editor.defaultFormatter": "esbenp.prettier-vscode"
23 | },
24 | "cSpell.words": ["Vesktop", "Sunroof"]
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EOL (read, thanks)
2 | Use [Equibop](https://github.com/Equicord/equibop) now.
3 | Suncord has gone EOL. Because I don't always have the time to keep up with what people suggest or such and it's easier for me to work on [Equicord](https://github.com/Equicord/Equicord)/[Equibop](https://github.com/Equicord/Equibop) with other people, the source and everything will stay here and you can still use it, but it'll probably become outdated after some time and break.
4 |
5 | # Sunroof [ ](https://github.com/verticalsync/Sunroof)
6 |
7 | [](https://github.com/verticalsync/Suncord)
8 | [](https://github.com/verticalsync/Sunroof/actions/workflows/test.yml)
9 | [](https://discord.gg/VasF3Ma4Ab)
10 |
11 | Sunroof is a fork of [Vesktop](https://github.com/Vencord/Vesktop).
12 |
13 | You can join our [discord server](https://discord.gg/VasF3Ma4Ab) for commits, changes, chat or even support.
14 |
15 | ## Main features
16 |
17 | - Much more lightweight and faster than the official Discord app
18 | - Linux Screenshare with sound & wayland
19 |
20 | **Extra included changes**
21 |
22 | - Suncord preinstalled
23 | - Custom Splash animations from [this PR](https://github.com/Vencord/Vesktop/pull/355)
24 |
25 | **Not yet supported**:
26 |
27 | - Global Keybinds
28 |
29 | ## Installing
30 |
31 | ### Windows
32 |
33 | If you don't know the difference, pick the Installer.
34 |
35 | - [Installer](https://github.com/verticalsync/Sunroof/releases/latest/download/Sunroof-Setup-1.6.1.exe)
36 | - Portable
37 | - [x64 / amd64](https://github.com/verticalsync/Sunroof/releases/latest/download/Sunroof-1.6.1-win.zip)
38 | - [arm64](https://github.com/verticalsync/Sunroof/releases/download/v1.6.1/Sunroof-1.6.1-arm64-win.zip)
39 |
40 | ### Mac
41 |
42 | If you don't know the difference, pick the Intel build.
43 |
44 | - [Sunroof.dmg](https://github.com/verticalsync/Sunroof/releases/download/v1.6.1/Sunroof-1.6.1-universal.dmg)
45 |
46 | ### Linux
47 |
48 | [](https://flathub.org/apps/io.github.verticalsync.sunroof)
49 |
50 | If you don't know the difference, pick amd64.
51 |
52 | - amd64 / x86_64
53 | - [AppImage](https://github.com/verticalsync/Sunroof/releases/latest/download/Sunroof-1.6.1.AppImage)
54 | - [Ubuntu/Debian (.deb)](https://github.com/verticalsync/Sunroof/releases/latest/download/sunroof_1.6.1_amd64.deb)
55 | - [Fedora/RHEL (.rpm)](https://github.com/verticalsync/Sunroof/releases/latest/download/sunroof-1.6.1.x86_64.rpm)
56 | - [tarball](https://github.com/verticalsync/Sunroof/releases/latest/download/sunroof-1.6.1.tar.gz)
57 | - arm64 / aarch64
58 | - [AppImage](https://github.com/verticalsync/Sunroof/releases/latest/download/Sunroof-1.6.1-arm64.AppImage)
59 | - [Ubuntu/Debian (.deb)](https://github.com/verticalsync/Sunroof/releases/latest/download/sunroof_1.6.1_arm64.deb)
60 | - [Fedora/RHEL (.rpm)](https://github.com/verticalsync/Sunroof/releases/latest/download/sunroof-1.6.1.aarch64.rpm)
61 | - [tarball](https://github.com/verticalsync/Sunroof/releases/latest/download/sunroof-1.6.1-arm64.tar.gz)
62 |
63 | ## Sponsors
64 |
65 | | **Thanks a lot to all Suncord [sponsors](https://github.com/sponsors/verticalsync)!!** |
66 | | :---------------------------------------------------------------------------------------------------------------------------------: |
67 | | [](https://github.com/sponsors/verticalsync) |
68 | | _generated using forked [github-sponsor-graph](https://github.com/verticalsync/github-sponsor-graph)_ |
69 |
70 | ## Building from Source
71 |
72 | Packaging will create builds in the dist/ folder
73 |
74 | > [!NOTE]
75 | > On Windows, if you run the test script, you will get test errors about venmic, you can ignore these as it's a linux only module.
76 |
77 | ```sh
78 | git clone https://github.com/verticalsync/Sunroof
79 | cd Sunroof
80 |
81 | # Install Dependencies
82 | pnpm i
83 |
84 | # Either run it without packaging
85 | pnpm start
86 |
87 | # Or package
88 | pnpm package
89 | # Or only build the pacman target
90 | pnpm package --linux pacman
91 | # Or package to a directory only
92 | pnpm package:dir
93 | ```
94 |
--------------------------------------------------------------------------------
/build/background.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/build/background.tiff
--------------------------------------------------------------------------------
/build/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | com.apple.security.cs.allow-unsigned-executable-memory
5 |
6 | com.apple.security.cs.allow-jit
7 |
8 | com.apple.security.network.client
9 |
10 | com.apple.security.device.audio-input
11 |
12 | com.apple.security.device.camera
13 |
14 | com.apple.security.device.bluetooth
15 |
16 | com.apple.security.cs.allow-dyld-environment-variables
17 |
18 | com.apple.security.cs.disable-library-validation
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/build/icon.png
--------------------------------------------------------------------------------
/build/installer.nsh:
--------------------------------------------------------------------------------
1 | !macro preInit
2 | SetRegView 64
3 | WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\sunroof"
4 | WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\sunroof"
5 | SetRegView 32
6 | WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\sunroof"
7 | WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\sunroof"
8 | !macroend
9 |
--------------------------------------------------------------------------------
/meta/io.github.verticalsync.sunroof.metainfo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | io.github.verticalsync.sunroof
5 | Sunroof
6 | Snappier Discord app with Suncord
7 | Suncord Contributors
8 | io.github.verticalsync.sunroof.desktop
9 | CC0-1.0
10 | GPL-3.0
11 | Suncord
12 |
13 | Sunroof is a cross-platform Vesktop Fork desktop app aiming to give you extra plugins but still give you a snappier Discord experience, with Suncord pre-installed.
14 | Which comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.
15 |
16 |
17 |
18 | Suncord settings page and about window open
19 | https://files.catbox.moe/59wp6h.png
20 |
21 |
22 | A dialog showing screenshare options
23 | https://files.catbox.moe/nbo64w.png
24 |
25 |
26 | A screenshot of a Discord server
27 | https://files.catbox.moe/uzpk4h.png
28 |
29 |
30 |
31 |
32 | https://github.com/verticalsync/Sunroof/releases/tag/v1.6.1
33 |
34 | Add new commits from Vesktop
35 |
36 |
37 |
38 | https://github.com/verticalsync/Sunroof/releases/tag/1.6.0
39 |
40 | Add new commits from Vesktop
41 |
42 |
43 |
44 | https://github.com/verticalsync/Sunroof/releases/tag/1.5.9
45 |
46 | Re-add support for MacOS
47 |
48 |
49 |
50 | https://github.com/verticalsync/Sunroof/releases/tag/1.5.7
51 |
52 | New Features
53 |
54 | Able to change the splash image to anything you want
55 |
56 |
57 |
58 |
59 | https://github.com/verticalsync/Sunroof/releases/tag/1.5.6
60 |
61 | Fixes
62 |
63 | Fix screenshare and some other things not working
64 |
65 |
66 |
67 |
68 | https://github.com/verticalsync/Sunroof/releases/tag/1.5.5
69 |
70 | New Features
71 |
72 | Added categories to Vesktop settings to reduce visual clutter by @justin13888
73 | Added support for Vencord's transparent window options
74 |
75 | Fixes
76 |
77 | Fixed ugly error popups when starting Vesktop without working internet connection
78 | Fixed popout title bars on Windows
79 | Fixed spellcheck entries
80 | Fixed screenshare audio using microphone on debian, by @Curve
81 | Fixed a bug where autostart on Linux won't preserve command line flags
82 |
83 |
84 |
85 |
86 | https://github.com/Vencord/Vesktop/releases/tag/v1.5.0
87 |
88 | What's Changed
89 |
90 | fully renamed to Vesktop. You will likely have to login to Discord again. You might have to re-create your vesktop shortcut
91 | added option to disable smooth scrolling by @ZirixCZ
92 | added setting to disable hardware acceleration by @zt64
93 | fixed adding connections
94 | fixed / improved discord popouts
95 | you can now use the custom discord titlebar on linux/mac
96 | the splash window is now draggable
97 | now signed on mac
98 |
99 |
100 |
101 |
102 | https://github.com/Vencord/Vesktop/releases/tag/v0.4.4
103 |
104 | What's Changed
105 |
106 | improve venmic system compatibility by @Curve
107 | Update steamdeck controller layout by @AAGaming00
108 | feat: Add option to disable smooth scrolling by @ZirixCZ
109 | unblur shiggy in splash screen by @viacoro
110 | update electron & arrpc @D3SOX
111 |
112 |
113 |
114 |
115 | https://github.com/Vencord/Vesktop/releases/tag/v0.4.3
116 |
117 |
118 | https://github.com/Vencord/Vesktop/releases/tag/v0.4.2
119 |
120 |
121 | https://github.com/Vencord/Vesktop/releases/tag/v0.4.1
122 |
123 |
124 | https://github.com/Vencord/Vesktop/releases/tag/v0.4.0
125 |
126 |
127 | https://github.com/Vencord/Vesktop/releases/tag/v0.3.3
128 |
129 |
130 | https://github.com/Vencord/Vesktop/releases/tag/v0.3.2
131 |
132 |
133 | https://github.com/Vencord/Vesktop/releases/tag/v0.3.1
134 |
135 |
136 | https://github.com/Vencord/Vesktop/releases/tag/v0.3.0
137 |
138 |
139 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.9
140 |
141 |
142 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.8
143 |
144 |
145 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.7
146 |
147 |
148 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.6
149 |
150 |
151 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.5
152 |
153 |
154 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.4
155 |
156 |
157 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.3
158 |
159 |
160 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.2
161 |
162 |
163 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.1
164 |
165 |
166 | https://github.com/Vencord/Vesktop/releases/tag/v0.2.0
167 |
168 |
169 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.9
170 |
171 |
172 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.8
173 |
174 |
175 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.7
176 |
177 |
178 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.6
179 |
180 |
181 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.5
182 |
183 |
184 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.4
185 |
186 |
187 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.3
188 |
189 |
190 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.2
191 |
192 |
193 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.1
194 |
195 |
196 | https://github.com/Vencord/Vesktop/releases/tag/v0.1.0
197 |
198 |
199 | https://github.com/verticalsync/Sunroof
200 | https://github.com/verticalsync/Sunroof/issues
201 | https://github.com/verticalsync/Sunroof/issues
202 | https://github.com/sponsors/verticalsync
203 | https://github.com/verticalsync/Sunroof
204 |
205 | InstantMessaging
206 | Network
207 |
208 |
209 | pointing
210 | keyboard
211 | 420
212 | always
213 |
214 |
215 | voice
216 | 760
217 | 1200
218 |
219 |
220 | intense
221 | intense
222 | intense
223 | intense
224 |
225 |
226 | Discord
227 | Sunroof
228 | Suncord
229 | Vencord
230 | Vesktop
231 | Privacy
232 | Mod
233 |
234 |
235 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sunroof",
3 | "version": "1.6.1",
4 | "private": true,
5 | "description": "A fork of Vesktop pre-packaged with Suncord",
6 | "keywords": [],
7 | "homepage": "https://github.com/verticalsync/sunroof",
8 | "license": "GPL-3.0",
9 | "author": "verticalsync",
10 | "main": "dist/js/main.js",
11 | "scripts": {
12 | "build": "tsx scripts/build/build.mts",
13 | "build:dev": "pnpm build --dev",
14 | "package": "pnpm build && electron-builder",
15 | "package:dir": "pnpm build && electron-builder --dir",
16 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.mts,.mjs",
17 | "lint:fix": "pnpm lint --fix",
18 | "start": "pnpm build && electron .",
19 | "start:dev": "pnpm build:dev && electron .",
20 | "start:watch": "pnpm build:dev && tsx scripts/startWatch.mts",
21 | "test": "pnpm lint && pnpm testTypes",
22 | "testTypes": "tsc --noEmit",
23 | "watch": "pnpm build --watch",
24 | "updateMeta": "tsx scripts/utils/updateMeta.mts"
25 | },
26 | "dependencies": {
27 | "arrpc": "github:OpenAsar/arrpc#c62ec6a04c8d870530aa6944257fe745f6c59a24",
28 | "electron-updater": "^6.2.1"
29 | },
30 | "optionalDependencies": {
31 | "@vencord/venmic": "^6.1.0"
32 | },
33 | "devDependencies": {
34 | "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
35 | "@types/node": "^20.11.26",
36 | "@types/react": "^18.2.0",
37 | "@typescript-eslint/eslint-plugin": "^7.2.0",
38 | "@typescript-eslint/parser": "^7.2.0",
39 | "@vencord/types": "^1.8.4",
40 | "dotenv": "^16.4.5",
41 | "electron": "^31.1.0",
42 | "electron-builder": "^24.13.3",
43 | "esbuild": "^0.20.1",
44 | "eslint": "^8.57.0",
45 | "eslint-config-prettier": "^9.1.0",
46 | "eslint-import-resolver-alias": "^1.1.2",
47 | "eslint-plugin-license-header": "^0.6.0",
48 | "eslint-plugin-path-alias": "^1.0.0",
49 | "eslint-plugin-prettier": "^5.1.3",
50 | "eslint-plugin-simple-import-sort": "^12.0.0",
51 | "eslint-plugin-unused-imports": "^3.1.0",
52 | "prettier": "^3.2.5",
53 | "source-map-support": "^0.5.21",
54 | "tsx": "^4.7.1",
55 | "type-fest": "^4.12.0",
56 | "typescript": "^5.4.2",
57 | "xml-formatter": "^3.6.2"
58 | },
59 | "packageManager": "pnpm@9.1.0",
60 | "engines": {
61 | "node": ">=18",
62 | "pnpm": ">=9"
63 | },
64 | "build": {
65 | "appId": "io.github.verticalsync.sunroof",
66 | "productName": "Sunroof",
67 | "files": [
68 | "!*",
69 | "!node_modules",
70 | "dist/js",
71 | "static",
72 | "package.json",
73 | "LICENSE"
74 | ],
75 | "beforePack": "scripts/build/sandboxFix.js",
76 | "linux": {
77 | "icon": "build/icon.icns",
78 | "category": "Network",
79 | "maintainer": "rcx@riseup.net",
80 | "target": [
81 | {
82 | "target": "deb",
83 | "arch": [
84 | "x64",
85 | "arm64"
86 | ]
87 | },
88 | {
89 | "target": "tar.gz",
90 | "arch": [
91 | "x64",
92 | "arm64"
93 | ]
94 | },
95 | {
96 | "target": "rpm",
97 | "arch": [
98 | "x64",
99 | "arm64"
100 | ]
101 | },
102 | {
103 | "target": "AppImage",
104 | "arch": [
105 | "x64",
106 | "arm64"
107 | ]
108 | }
109 | ],
110 | "desktop": {
111 | "Name": "Sunroof",
112 | "GenericName": "Internet Messenger",
113 | "Type": "Application",
114 | "Categories": "Network;InstantMessaging;Chat;",
115 | "Keywords": "discord;sunroof;vesktop;vencord;suncord;electron;chat;"
116 | }
117 | },
118 | "mac": {
119 | "target": [
120 | {
121 | "target": "default",
122 | "arch": [
123 | "universal"
124 | ]
125 | }
126 | ],
127 | "category": "Network",
128 | "extendInfo": {
129 | "NSMicrophoneUsageDescription": "This app needs access to the microphone",
130 | "NSCameraUsageDescription": "This app needs access to the camera",
131 | "com.apple.security.device.audio-input": true,
132 | "com.apple.security.device.camera": true
133 | }
134 | },
135 | "dmg": {
136 | "background": "build/background.tiff",
137 | "icon": "build/icon.icns",
138 | "iconSize": 105,
139 | "window": {
140 | "width": 512,
141 | "height": 340
142 | },
143 | "contents": [
144 | {
145 | "x": 140,
146 | "y": 160
147 | },
148 | {
149 | "x": 372,
150 | "y": 160,
151 | "type": "link",
152 | "path": "/Applications"
153 | }
154 | ]
155 | },
156 | "nsis": {
157 | "include": "build/installer.nsh",
158 | "oneClick": false
159 | },
160 | "win": {
161 | "target": [
162 | {
163 | "target": "nsis",
164 | "arch": [
165 | "x64",
166 | "arm64"
167 | ]
168 | },
169 | {
170 | "target": "zip",
171 | "arch": [
172 | "x64",
173 | "arm64"
174 | ]
175 | }
176 | ]
177 | },
178 | "publish": {
179 | "provider": "github"
180 | }
181 | },
182 | "pnpm": {
183 | "patchedDependencies": {
184 | "arrpc@3.4.0": "patches/arrpc@3.4.0.patch"
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/patches/arrpc@3.4.0.patch:
--------------------------------------------------------------------------------
1 | diff --git a/src/process/index.js b/src/process/index.js
2 | index 97ea6514b54dd9c5df588c78f0397d31ab5f882a..c2bdbd6aaa5611bc6ff1d993beeb380b1f5ec575 100644
3 | --- a/src/process/index.js
4 | +++ b/src/process/index.js
5 | @@ -5,8 +5,7 @@ import fs from 'node:fs';
6 | import { dirname, join } from 'path';
7 | import { fileURLToPath } from 'url';
8 |
9 | -const __dirname = dirname(fileURLToPath(import.meta.url));
10 | -const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
11 | +const DetectableDB = require('./detectable.json');
12 |
13 | import * as Natives from './native/index.js';
14 | const Native = Natives[process.platform];
15 |
--------------------------------------------------------------------------------
/scripts/build/build.mts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { BuildContext, BuildOptions, context } from "esbuild";
8 | import { copyFile } from "fs/promises";
9 |
10 | import vencordDep from "./vencordDep.mjs";
11 |
12 | const isDev = process.argv.includes("--dev");
13 |
14 | const CommonOpts: BuildOptions = {
15 | minify: !isDev,
16 | bundle: true,
17 | sourcemap: "linked",
18 | logLevel: "info"
19 | };
20 |
21 | const NodeCommonOpts: BuildOptions = {
22 | ...CommonOpts,
23 | format: "cjs",
24 | platform: "node",
25 | external: ["electron"],
26 | target: ["esnext"],
27 | define: {
28 | IS_DEV: JSON.stringify(isDev)
29 | }
30 | };
31 |
32 | const contexts = [] as BuildContext[];
33 | async function createContext(options: BuildOptions) {
34 | contexts.push(await context(options));
35 | }
36 |
37 | async function copyVenmic() {
38 | if (process.platform !== "linux") return;
39 |
40 | return Promise.all([
41 | copyFile(
42 | "./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-x64/node-napi-v7.node",
43 | "./static/dist/venmic-x64.node"
44 | ),
45 | copyFile(
46 | "./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-arm64/node-napi-v7.node",
47 | "./static/dist/venmic-arm64.node"
48 | )
49 | ]).catch(() => console.warn("Failed to copy venmic. Building without venmic support"));
50 | }
51 |
52 | await Promise.all([
53 | copyVenmic(),
54 | createContext({
55 | ...NodeCommonOpts,
56 | entryPoints: ["src/main/index.ts"],
57 | outfile: "dist/js/main.js",
58 | footer: { js: "//# sourceURL=VCDMain" }
59 | }),
60 | createContext({
61 | ...NodeCommonOpts,
62 | entryPoints: ["src/preload/index.ts"],
63 | outfile: "dist/js/preload.js",
64 | footer: { js: "//# sourceURL=VCDPreload" }
65 | }),
66 | createContext({
67 | ...CommonOpts,
68 | globalName: "Sunroof",
69 | entryPoints: ["src/renderer/index.ts"],
70 | outfile: "dist/js/renderer.js",
71 | format: "iife",
72 | inject: ["./scripts/build/injectReact.mjs"],
73 | jsxFactory: "VencordCreateElement",
74 | jsxFragment: "VencordFragment",
75 | external: ["@vencord/types/*"],
76 | plugins: [vencordDep],
77 | footer: { js: "//# sourceURL=VCDRenderer" }
78 | })
79 | ]);
80 |
81 | const watch = process.argv.includes("--watch");
82 |
83 | if (watch) {
84 | await Promise.all(contexts.map(ctx => ctx.watch()));
85 | } else {
86 | await Promise.all(
87 | contexts.map(async ctx => {
88 | await ctx.rebuild();
89 | await ctx.dispose();
90 | })
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/scripts/build/injectReact.mjs:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
8 | export let VencordCreateElement = (...args) =>
9 | (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
10 |
--------------------------------------------------------------------------------
/scripts/build/sandboxFix.js:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | // Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
8 |
9 | const fs = require("fs/promises");
10 | const path = require("path");
11 | let isApplied = false;
12 |
13 | const hook = async () => {
14 | if (isApplied) return;
15 | isApplied = true;
16 | if (process.platform !== "linux") {
17 | // this fix is only required on linux
18 | return;
19 | }
20 | const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget");
21 | const oldBuildMethod = AppImageTarget.default.prototype.build;
22 | AppImageTarget.default.prototype.build = async function (...args) {
23 | console.log("Running AppImage builder hook", args);
24 | const oldPath = args[0];
25 | const newPath = oldPath + "-appimage-sandbox-fix";
26 | // just in case
27 | try {
28 | await fs.rm(newPath, {
29 | recursive: true
30 | });
31 | } catch {}
32 |
33 | console.log("Copying to apply appimage fix", oldPath, newPath);
34 | await fs.cp(oldPath, newPath, {
35 | recursive: true
36 | });
37 | args[0] = newPath;
38 |
39 | const executable = path.join(newPath, this.packager.executableName);
40 |
41 | const loaderScript = `
42 | #!/usr/bin/env bash
43 |
44 | SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
45 | IS_STEAMOS=0
46 |
47 | if [[ "$SteamOS" == "1" && "$SteamGamepadUI" == "1" ]]; then
48 | echo "Running Sunroof on SteamOS, disabling sandbox"
49 | IS_STEAMOS=1
50 | fi
51 |
52 | exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ] && echo '--no-sandbox')" "$@"
53 | `.trim();
54 |
55 | try {
56 | await fs.rename(executable, executable + ".bin");
57 | await fs.writeFile(executable, loaderScript);
58 | await fs.chmod(executable, 0o755);
59 | } catch (e) {
60 | console.error("failed to create loder for sandbox fix: " + e.message);
61 | throw new Error("Failed to create loader for sandbox fix");
62 | }
63 |
64 | const ret = await oldBuildMethod.apply(this, args);
65 |
66 | await fs.rm(newPath, {
67 | recursive: true
68 | });
69 |
70 | return ret;
71 | };
72 | };
73 |
74 | module.exports = hook;
75 |
--------------------------------------------------------------------------------
/scripts/build/vencordDep.mts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { globalExternalsWithRegExp } from "@fal-works/esbuild-plugin-global-externals";
8 |
9 | const names = {
10 | webpack: "Vencord.Webpack",
11 | "webpack/common": "Vencord.Webpack.Common",
12 | utils: "Vencord.Util",
13 | api: "Vencord.Api",
14 | "api/settings": "Vencord",
15 | components: "Vencord.Components"
16 | };
17 |
18 | export default globalExternalsWithRegExp({
19 | getModuleInfo(modulePath) {
20 | const path = modulePath.replace("@vencord/types/", "");
21 | let varName = names[path];
22 | if (!varName) {
23 | const altMapping = names[path.split("/")[0]];
24 | if (!altMapping) throw new Error("Unknown module path: " + modulePath);
25 |
26 | varName =
27 | altMapping +
28 | "." +
29 | // @ts-ignore
30 | path.split("/")[1].replaceAll("/", ".");
31 | }
32 | return {
33 | varName,
34 | type: "cjs"
35 | };
36 | },
37 | modulePathFilter: /^@vencord\/types.+$/
38 | });
39 |
--------------------------------------------------------------------------------
/scripts/header.txt:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
--------------------------------------------------------------------------------
/scripts/start.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import "./utils/dotenv";
8 |
9 | import { spawnNodeModuleBin } from "./utils/spawn.mjs";
10 |
11 | spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]);
12 |
--------------------------------------------------------------------------------
/scripts/startWatch.mts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import "./start";
8 |
9 | import { spawnNodeModuleBin } from "./utils/spawn.mjs";
10 | spawnNodeModuleBin("tsx", ["scripts/build/build.mts", "--", "--watch", "--dev"]);
11 |
--------------------------------------------------------------------------------
/scripts/utils/dotenv.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { config } from "dotenv";
8 |
9 | config();
10 |
--------------------------------------------------------------------------------
/scripts/utils/spawn.mts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { spawn as spaaawn, SpawnOptions } from "child_process";
8 | import { join } from "path";
9 |
10 | const EXT = process.platform === "win32" ? ".cmd" : "";
11 |
12 | const OPTS: SpawnOptions = {
13 | stdio: "inherit"
14 | };
15 |
16 | export function spawnNodeModuleBin(bin: string, args: string[]) {
17 | spaaawn(join("node_modules", ".bin", bin + EXT), args, OPTS);
18 | }
19 |
--------------------------------------------------------------------------------
/scripts/utils/updateMeta.mts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { promises as fs } from "node:fs";
8 |
9 | import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
10 | import xmlFormat from "xml-formatter";
11 |
12 | function generateDescription(description: string, descriptionNode: Element) {
13 | const lines = description.replace(/\r/g, "").split("\n");
14 | let currentList: Element | null = null;
15 |
16 | for (let i = 0; i < lines.length; i++) {
17 | const line = lines[i];
18 |
19 | if (line.includes("New Contributors")) {
20 | // we're done, don't parse any more since the new contributors section is the last one
21 | break;
22 | }
23 |
24 | if (line.startsWith("## ")) {
25 | const pNode = descriptionNode.ownerDocument.createElement("p");
26 | pNode.textContent = line.slice(3);
27 | descriptionNode.appendChild(pNode);
28 | } else if (line.startsWith("* ")) {
29 | const liNode = descriptionNode.ownerDocument.createElement("li");
30 | liNode.textContent = line.slice(2).split("in https://github.com")[0].trim(); // don't include links to github
31 |
32 | if (!currentList) {
33 | currentList = descriptionNode.ownerDocument.createElement("ul");
34 | }
35 |
36 | currentList.appendChild(liNode);
37 | }
38 |
39 | if (currentList && !lines[i + 1].startsWith("* ")) {
40 | descriptionNode.appendChild(currentList);
41 | currentList = null;
42 | }
43 | }
44 | }
45 |
46 | const latestReleaseInformation = await fetch("https://api.github.com/repos/verticalsync/Sunroof/releases/latest", {
47 | headers: {
48 | Accept: "application/vnd.github+json",
49 | "X-Github-Api-Version": "2022-11-28"
50 | }
51 | }).then(res => res.json());
52 |
53 | const metaInfo = await fs.readFile("./meta/io.github.verticalsync.sunroof.metainfo.xml", "utf-8");
54 |
55 | const parser = new DOMParser().parseFromString(metaInfo, "text/xml");
56 |
57 | const releaseList = parser.getElementsByTagName("releases")[0];
58 |
59 | for (let i = 0; i < releaseList.childNodes.length; i++) {
60 | const release = releaseList.childNodes[i] as Element;
61 |
62 | if (release.nodeType === 1 && release.getAttribute("version") === latestReleaseInformation.name) {
63 | console.log("Latest release already added, nothing to be done");
64 | process.exit(0);
65 | }
66 | }
67 |
68 | const release = parser.createElement("release");
69 | release.setAttribute("version", latestReleaseInformation.name);
70 | release.setAttribute("date", latestReleaseInformation.published_at.split("T")[0]);
71 | release.setAttribute("type", "stable");
72 |
73 | const releaseUrl = parser.createElement("url");
74 | releaseUrl.textContent = latestReleaseInformation.html_url;
75 |
76 | release.appendChild(releaseUrl);
77 |
78 | const description = parser.createElement("description");
79 |
80 | // we're not using a full markdown parser here since we don't have a lot of formatting options to begin with
81 | generateDescription(latestReleaseInformation.body, description);
82 |
83 | release.appendChild(description);
84 |
85 | releaseList.insertBefore(release, releaseList.childNodes[0]);
86 |
87 | const output = xmlFormat(new XMLSerializer().serializeToString(parser), {
88 | lineSeparator: "\n",
89 | collapseContent: true,
90 | indentation: " "
91 | });
92 |
93 | await fs.writeFile("./meta/io.github.verticalsync.sunroof.metainfo.xml", output, "utf-8");
94 |
--------------------------------------------------------------------------------
/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | declare global {
8 | export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative;
9 | export var Vesktop: typeof import("renderer/index");
10 | export var VCDP: any;
11 |
12 | export var IS_DEV: boolean;
13 | }
14 |
15 | export {};
16 |
--------------------------------------------------------------------------------
/src/main/about.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { BrowserWindow } from "electron";
8 | import { join } from "path";
9 | import { ICON_PATH, VIEW_DIR } from "shared/paths";
10 |
11 | import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
12 |
13 | export function createAboutWindow() {
14 | const about = new BrowserWindow({
15 | center: true,
16 | autoHideMenuBar: true,
17 | icon: ICON_PATH,
18 | webPreferences: {
19 | preload: join(__dirname, "updaterPreload.js")
20 | }
21 | });
22 |
23 | makeLinksOpenExternally(about);
24 |
25 | about.loadFile(join(VIEW_DIR, "about.html"));
26 |
27 | return about;
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/appBadge.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { app, NativeImage, nativeImage } from "electron";
8 | import { join } from "path";
9 | import { BADGE_DIR } from "shared/paths";
10 |
11 | const imgCache = new Map();
12 | function loadBadge(index: number) {
13 | const cached = imgCache.get(index);
14 | if (cached) return cached;
15 |
16 | const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.ico`));
17 | imgCache.set(index, img);
18 |
19 | return img;
20 | }
21 |
22 | let lastIndex: null | number = -1;
23 |
24 | export function setBadgeCount(count: number) {
25 | switch (process.platform) {
26 | case "linux":
27 | if (count === -1) count = 0;
28 | app.setBadgeCount(count);
29 | break;
30 | case "darwin":
31 | if (count === 0) {
32 | app.dock.setBadge("");
33 | break;
34 | }
35 | app.dock.setBadge(count === -1 ? "•" : count.toString());
36 | break;
37 | case "win32":
38 | const [index, description] = getBadgeIndexAndDescription(count);
39 | if (lastIndex === index) break;
40 |
41 | lastIndex = index;
42 |
43 | // circular import shenanigans
44 | const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
45 | mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
46 | break;
47 | }
48 | }
49 |
50 | function getBadgeIndexAndDescription(count: number): [number | null, string] {
51 | if (count === -1) return [11, "Unread Messages"];
52 | if (count === 0) return [null, "No Notifications"];
53 |
54 | const index = Math.max(1, Math.min(count, 10));
55 | return [index, `${index} Notification`];
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/arrpc.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import Server from "arrpc";
8 | import { IpcEvents } from "shared/IpcEvents";
9 |
10 | import { mainWin } from "./mainWindow";
11 | import { Settings } from "./settings";
12 |
13 | let server: any;
14 |
15 | const inviteCodeRegex = /^(\w|-)+$/;
16 |
17 | export async function initArRPC() {
18 | if (server || !Settings.store.arRPC) return;
19 |
20 | try {
21 | server = await new Server();
22 | server.on("activity", (data: any) => mainWin.webContents.send(IpcEvents.ARRPC_ACTIVITY, JSON.stringify(data)));
23 | server.on("invite", (invite: string, callback: (valid: boolean) => void) => {
24 | invite = String(invite);
25 | if (!inviteCodeRegex.test(invite)) return callback(false);
26 |
27 | mainWin.webContents
28 | // Safety: Result of JSON.stringify should always be safe to equal
29 | // Also, just to be super super safe, invite is regex validated above
30 | .executeJavaScript(`Sunroof.openInviteModal(${JSON.stringify(invite)})`)
31 | .then(callback);
32 | });
33 | } catch (e) {
34 | console.error("Failed to start arRPC server", e);
35 | }
36 | }
37 |
38 | Settings.addChangeListener("arRPC", initArRPC);
39 |
--------------------------------------------------------------------------------
/src/main/autoStart.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { app } from "electron";
8 | import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
9 | import { join } from "path";
10 |
11 | interface AutoStart {
12 | isEnabled(): boolean;
13 | enable(): void;
14 | disable(): void;
15 | }
16 |
17 | function makeAutoStartLinux(): AutoStart {
18 | const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
19 | const dir = join(configDir, "autostart");
20 | const file = join(dir, "sunroof.desktop");
21 |
22 | // IM STUPID
23 | const legacyName = join(dir, "suncord.desktop");
24 | if (existsSync(legacyName)) renameSync(legacyName, file);
25 |
26 | // "Quoting must be done by enclosing the argument between double quotes and escaping the double quote character,
27 | // backtick character ("`"), dollar sign ("$") and backslash character ("\") by preceding it with an additional backslash character"
28 | // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
29 | const commandLine = process.argv.map(arg => '"' + arg.replace(/["$`\\]/g, "\\$&") + '"').join(" ");
30 |
31 | return {
32 | isEnabled: () => existsSync(file),
33 | enable() {
34 | const desktopFile = `
35 | [Desktop Entry]
36 | Type=Application
37 | Name=Sunroof
38 | Comment=Sunroof autostart script
39 | Exec=${commandLine}
40 | StartupNotify=false
41 | Terminal=false
42 | `.trim();
43 |
44 | mkdirSync(dir, { recursive: true });
45 | writeFileSync(file, desktopFile);
46 | },
47 | disable: () => rmSync(file, { force: true })
48 | };
49 | }
50 |
51 | const autoStartWindowsMac: AutoStart = {
52 | isEnabled: () => app.getLoginItemSettings().openAtLogin,
53 | enable: () => app.setLoginItemSettings({ openAtLogin: true }),
54 | disable: () => app.setLoginItemSettings({ openAtLogin: false })
55 | };
56 |
57 | export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac;
58 |
--------------------------------------------------------------------------------
/src/main/constants.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { app } from "electron";
8 | import { existsSync, mkdirSync, readdirSync, renameSync, rmdirSync } from "fs";
9 | import { dirname, join } from "path";
10 |
11 | const vesktopDir = dirname(process.execPath);
12 |
13 | export const PORTABLE =
14 | process.platform === "win32" &&
15 | !process.execPath.toLowerCase().endsWith("electron.exe") &&
16 | !existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
17 |
18 | const LEGACY_DATA_DIR = join(app.getPath("appData"), "SuncordDesktop", "SuncordDesktop");
19 | export const DATA_DIR =
20 | process.env.SUNCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
21 |
22 | mkdirSync(DATA_DIR, { recursive: true });
23 |
24 | // TODO: remove eventually
25 | if (existsSync(LEGACY_DATA_DIR)) {
26 | try {
27 | console.warn("Detected legacy settings dir", LEGACY_DATA_DIR + ".", "migrating to", DATA_DIR);
28 | for (const file of readdirSync(LEGACY_DATA_DIR)) {
29 | renameSync(join(LEGACY_DATA_DIR, file), join(DATA_DIR, file));
30 | }
31 | rmdirSync(LEGACY_DATA_DIR);
32 | renameSync(
33 | join(app.getPath("appData"), "SuncordDesktop", "IndexedDB"),
34 | join(DATA_DIR, "sessionData", "IndexedDB")
35 | );
36 | } catch (e) {
37 | console.error("Migration failed", e);
38 | }
39 | }
40 | const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
41 | app.setPath("sessionData", SESSION_DATA_DIR);
42 |
43 | export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings");
44 | export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css");
45 | export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json");
46 | export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes");
47 |
48 | // needs to be inline require because of circular dependency
49 | // as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised
50 | export const VENCORD_FILES_DIR =
51 | (require("./settings") as typeof import("./settings")).State.store.vencordDir ||
52 | join(SESSION_DATA_DIR, "suncordFiles");
53 |
54 | export const USER_AGENT = `Sunroof/${app.getVersion()} (https://github.com/verticalsync/Sunroof)`;
55 |
56 | // dimensions shamelessly stolen from Discord Desktop :3
57 | export const MIN_WIDTH = 940;
58 | export const MIN_HEIGHT = 500;
59 | export const DEFAULT_WIDTH = 1280;
60 | export const DEFAULT_HEIGHT = 720;
61 |
62 | export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"];
63 |
64 | const BrowserUserAgents = {
65 | darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
66 | linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
67 | windows:
68 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
69 | };
70 |
71 | export const BrowserUserAgent = BrowserUserAgents[process.platform] || BrowserUserAgents.windows;
72 |
73 | export const enum MessageBoxChoice {
74 | Default,
75 | Cancel
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/firstLaunch.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { app } from "electron";
8 | import { BrowserWindow } from "electron/main";
9 | import { copyFileSync, mkdirSync, readdirSync } from "fs";
10 | import { join } from "path";
11 | import { SplashProps } from "shared/browserWinProperties";
12 | import { ICON_PATH, VIEW_DIR } from "shared/paths";
13 |
14 | import { autoStart } from "./autoStart";
15 | import { DATA_DIR } from "./constants";
16 | import { createWindows } from "./mainWindow";
17 | import { Settings, State } from "./settings";
18 | import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
19 |
20 | interface Data {
21 | discordBranch: "stable" | "canary" | "ptb";
22 | minimizeToTray?: "on";
23 | autoStart?: "on";
24 | importSettings?: "on";
25 | richPresence?: "on";
26 | }
27 |
28 | export function createFirstLaunchTour() {
29 | const win = new BrowserWindow({
30 | ...SplashProps,
31 | frame: true,
32 | autoHideMenuBar: true,
33 | height: 470,
34 | width: 550,
35 | icon: ICON_PATH
36 | });
37 |
38 | makeLinksOpenExternally(win);
39 |
40 | win.loadFile(join(VIEW_DIR, "first-launch.html"));
41 | win.webContents.addListener("console-message", (_e, _l, msg) => {
42 | if (msg === "cancel") return app.exit();
43 |
44 | if (!msg.startsWith("form:")) return;
45 | const data = JSON.parse(msg.slice(5)) as Data;
46 |
47 | console.log(data);
48 | State.store.firstLaunch = false;
49 | Settings.store.discordBranch = data.discordBranch;
50 | Settings.store.minimizeToTray = !!data.minimizeToTray;
51 | Settings.store.arRPC = !!data.richPresence;
52 |
53 | if (data.autoStart) autoStart.enable();
54 |
55 | if (data.importSettings) {
56 | const from = join(app.getPath("userData"), "..", "Suncord", "settings");
57 | const to = join(DATA_DIR, "settings");
58 | try {
59 | const files = readdirSync(from);
60 | mkdirSync(to, { recursive: true });
61 |
62 | for (const file of files) {
63 | copyFileSync(join(from, file), join(to, file));
64 | }
65 | } catch (e) {
66 | console.error("Failed to import settings:", e);
67 | }
68 | }
69 |
70 | win.close();
71 |
72 | createWindows();
73 | });
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import "./ipc";
8 |
9 | import { app, BrowserWindow, nativeTheme, net, protocol } from "electron";
10 | import { autoUpdater } from "electron-updater";
11 |
12 | import { DATA_DIR } from "./constants";
13 | import { createFirstLaunchTour } from "./firstLaunch";
14 | import { createWindows, mainWin } from "./mainWindow";
15 | import { registerMediaPermissionsHandler } from "./mediaPermissions";
16 | import { registerScreenShareHandler } from "./screenShare";
17 | import { Settings, State } from "./settings";
18 | import { isDeckGameMode } from "./utils/steamOS";
19 |
20 | if (IS_DEV) {
21 | require("source-map-support").install();
22 | } else {
23 | autoUpdater.checkForUpdatesAndNotify();
24 | }
25 |
26 | // Make the Vencord files use our DATA_DIR
27 | process.env.SUNCORD_USER_DATA_DIR = DATA_DIR;
28 |
29 | function init() {
30 | const { disableSmoothScroll, hardwareAcceleration, splashAnimationPath } = Settings.store;
31 |
32 | const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(",");
33 | const disabledFeatures = app.commandLine.getSwitchValue("disable-features").split(",");
34 |
35 | if (hardwareAcceleration === false) {
36 | app.disableHardwareAcceleration();
37 | } else {
38 | enabledFeatures.push("VaapiVideoDecodeLinuxGL", "VaapiVideoEncoder", "VaapiVideoDecoder");
39 | }
40 |
41 | if (disableSmoothScroll) {
42 | app.commandLine.appendSwitch("disable-smooth-scrolling");
43 | }
44 |
45 | // disable renderer backgrounding to prevent the app from unloading when in the background
46 | // https://github.com/electron/electron/issues/2822
47 | // https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
48 | app.commandLine.appendSwitch("disable-renderer-backgrounding");
49 | app.commandLine.appendSwitch("disable-background-timer-throttling");
50 | app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
51 | if (process.platform === "win32") {
52 | disabledFeatures.push("CalculateNativeWinOcclusion");
53 | }
54 |
55 | // work around chrome 66 disabling autoplay by default
56 | app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
57 | // WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
58 | // HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
59 | //
60 | // WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
61 | disabledFeatures.push("WinRetrieveSuggestionsOnlyOnDemand", "HardwareMediaKeyHandling", "MediaSessionService");
62 |
63 | app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].filter(Boolean).join(","));
64 | app.commandLine.appendSwitch("disable-features", [...new Set(disabledFeatures)].filter(Boolean).join(","));
65 |
66 | // In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it
67 | if (isDeckGameMode) nativeTheme.themeSource = "dark";
68 |
69 | app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => {
70 | if (data.IS_DEV) app.quit();
71 | else if (mainWin) {
72 | if (mainWin.isMinimized()) mainWin.restore();
73 | if (!mainWin.isVisible()) mainWin.show();
74 | mainWin.focus();
75 | }
76 | });
77 |
78 | app.whenReady().then(async () => {
79 | if (process.platform === "win32") app.setAppUserModelId("io.github.verticalsync.sunroof");
80 |
81 | registerScreenShareHandler();
82 | registerMediaPermissionsHandler();
83 |
84 | // register file handler so we can load the custom splash animation from the user's filesystem
85 | protocol.handle("splash-animation", () => {
86 | return net.fetch("file:///" + splashAnimationPath);
87 | });
88 |
89 | bootstrap();
90 |
91 | app.on("activate", () => {
92 | if (BrowserWindow.getAllWindows().length === 0) createWindows();
93 | });
94 | });
95 | }
96 |
97 | if (!app.requestSingleInstanceLock({ IS_DEV })) {
98 | if (IS_DEV) {
99 | console.log("Sunroof is already running. Quitting previous instance...");
100 | init();
101 | } else {
102 | console.log("Sunroof is already running. Quitting...");
103 | app.quit();
104 | }
105 | } else {
106 | init();
107 | }
108 |
109 | async function bootstrap() {
110 | if (!Object.hasOwn(State.store, "firstLaunch")) {
111 | createFirstLaunchTour();
112 | } else {
113 | createWindows();
114 | }
115 | }
116 |
117 | app.on("window-all-closed", () => {
118 | if (process.platform !== "darwin") app.quit();
119 | });
120 |
--------------------------------------------------------------------------------
/src/main/ipc.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | if (process.platform === "linux") import("./venmic");
8 |
9 | import { execFile } from "child_process";
10 | import { app, BrowserWindow, clipboard, dialog, nativeImage, RelaunchOptions, session, shell } from "electron";
11 | import { mkdirSync, readFileSync, watch } from "fs";
12 | import { open, readFile } from "fs/promises";
13 | import { release } from "os";
14 | import { join } from "path";
15 | import { debounce } from "shared/utils/debounce";
16 |
17 | import { IpcEvents } from "../shared/IpcEvents";
18 | import { setBadgeCount } from "./appBadge";
19 | import { autoStart } from "./autoStart";
20 | import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
21 | import { mainWin } from "./mainWindow";
22 | import { Settings, State } from "./settings";
23 | import { handle, handleSync } from "./utils/ipcWrappers";
24 | import { PopoutWindows } from "./utils/popout";
25 | import { isDeckGameMode, showGamePage } from "./utils/steamOS";
26 | import { isValidVencordInstall } from "./utils/vencordLoader";
27 |
28 | handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"));
29 | handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () =>
30 | readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopRenderer.js"), "utf-8")
31 | );
32 |
33 | handleSync(IpcEvents.GET_RENDERER_SCRIPT, () => readFileSync(join(__dirname, "renderer.js"), "utf-8"));
34 | handleSync(IpcEvents.GET_RENDERER_CSS_FILE, () => join(__dirname, "renderer.css"));
35 |
36 | handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
37 | handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
38 |
39 | handleSync(
40 | IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
41 | () => process.platform === "win32" && Number(release().split(".").pop()) >= 22621
42 | );
43 |
44 | handleSync(IpcEvents.AUTOSTART_ENABLED, () => autoStart.isEnabled());
45 | handle(IpcEvents.ENABLE_AUTOSTART, autoStart.enable);
46 | handle(IpcEvents.DISABLE_AUTOSTART, autoStart.disable);
47 |
48 | handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: string) => {
49 | Settings.setData(settings, path);
50 | });
51 |
52 | handle(IpcEvents.RELAUNCH, async () => {
53 | const options: RelaunchOptions = {
54 | args: process.argv.slice(1).concat(["--relaunch"])
55 | };
56 | if (isDeckGameMode) {
57 | // We can't properly relaunch when running under gamescope, but we can at least navigate to our page in Steam.
58 | await showGamePage();
59 | } else if (app.isPackaged && process.env.APPIMAGE) {
60 | execFile(process.env.APPIMAGE, options.args);
61 | } else {
62 | app.relaunch(options);
63 | }
64 | app.exit();
65 | });
66 |
67 | handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
68 | shell.showItemInFolder(path);
69 | });
70 |
71 | handle(IpcEvents.FOCUS, () => {
72 | mainWin.show();
73 | mainWin.setSkipTaskbar(false);
74 | });
75 |
76 | handle(IpcEvents.CLOSE, (e, key?: string) => {
77 | const popout = PopoutWindows.get(key!);
78 | if (popout) return popout.close();
79 |
80 | const win = BrowserWindow.fromWebContents(e.sender) ?? e.sender;
81 | win.close();
82 | });
83 |
84 | handle(IpcEvents.MINIMIZE, e => {
85 | mainWin.minimize();
86 | });
87 |
88 | handle(IpcEvents.MAXIMIZE, e => {
89 | if (mainWin.isMaximized()) {
90 | mainWin.unmaximize();
91 | } else {
92 | mainWin.maximize();
93 | }
94 | });
95 |
96 | handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => {
97 | e.returnValue = session.defaultSession.availableSpellCheckerLanguages;
98 | });
99 |
100 | handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
101 | e.sender.replaceMisspelling(word);
102 | });
103 |
104 | handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
105 | e.sender.session.addWordToSpellCheckerDictionary(word);
106 | });
107 |
108 | handleSync(IpcEvents.GET_VENCORD_DIR, e => (e.returnValue = State.store.vencordDir));
109 |
110 | handle(IpcEvents.SELECT_VENCORD_DIR, async (_e, value?: null) => {
111 | if (value === null) {
112 | delete State.store.vencordDir;
113 | return "ok";
114 | }
115 |
116 | const res = await dialog.showOpenDialog(mainWin!, {
117 | properties: ["openDirectory"]
118 | });
119 | if (!res.filePaths.length) return "cancelled";
120 |
121 | const dir = res.filePaths[0];
122 | if (!isValidVencordInstall(dir)) return "invalid";
123 |
124 | State.store.vencordDir = dir;
125 |
126 | return "ok";
127 | });
128 |
129 | handle(IpcEvents.SELECT_IMAGE_PATH, async () => {
130 | const res = await dialog.showOpenDialog(mainWin!, {
131 | properties: ["openFile"],
132 | filters: [{ name: "Images", extensions: ["apng", "avif", "gif", "jpeg", "png", "svg", "webp"] }]
133 | });
134 | if (!res.filePaths.length) return "cancelled";
135 | return res.filePaths[0];
136 | });
137 |
138 | handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
139 |
140 | handle(IpcEvents.CLIPBOARD_COPY_IMAGE, async (_, buf: ArrayBuffer, src: string) => {
141 | clipboard.write({
142 | html: ` `,
143 | image: nativeImage.createFromBuffer(Buffer.from(buf))
144 | });
145 | });
146 |
147 | function readCss() {
148 | return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
149 | }
150 |
151 | open(VENCORD_QUICKCSS_FILE, "a+").then(fd => {
152 | fd.close();
153 | watch(
154 | VENCORD_QUICKCSS_FILE,
155 | { persistent: false },
156 | debounce(async () => {
157 | mainWin?.webContents.postMessage("VencordQuickCssUpdate", await readCss());
158 | }, 50)
159 | );
160 | });
161 |
162 | mkdirSync(VENCORD_THEMES_DIR, { recursive: true });
163 | watch(
164 | VENCORD_THEMES_DIR,
165 | { persistent: false },
166 | debounce(() => {
167 | mainWin?.webContents.postMessage("VencordThemeUpdate", void 0);
168 | })
169 | );
170 |
--------------------------------------------------------------------------------
/src/main/mainWindow.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import {
8 | app,
9 | BrowserWindow,
10 | BrowserWindowConstructorOptions,
11 | dialog,
12 | Menu,
13 | MenuItemConstructorOptions,
14 | nativeTheme,
15 | screen,
16 | session,
17 | Tray
18 | } from "electron";
19 | import { rm } from "fs/promises";
20 | import { join } from "path";
21 | import { IpcEvents } from "shared/IpcEvents";
22 | import { isTruthy } from "shared/utils/guards";
23 | import { once } from "shared/utils/once";
24 | import type { SettingsStore } from "shared/utils/SettingsStore";
25 |
26 | import { ICON_PATH } from "../shared/paths";
27 | import { createAboutWindow } from "./about";
28 | import { initArRPC } from "./arrpc";
29 | import {
30 | BrowserUserAgent,
31 | DATA_DIR,
32 | DEFAULT_HEIGHT,
33 | DEFAULT_WIDTH,
34 | MessageBoxChoice,
35 | MIN_HEIGHT,
36 | MIN_WIDTH,
37 | VENCORD_FILES_DIR
38 | } from "./constants";
39 | import { Settings, State, VencordSettings } from "./settings";
40 | import { createSplashWindow } from "./splash";
41 | import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
42 | import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
43 | import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
44 |
45 | let isQuitting = false;
46 | let tray: Tray;
47 |
48 | applyDeckKeyboardFix();
49 |
50 | app.on("before-quit", () => {
51 | isQuitting = true;
52 | });
53 |
54 | export let mainWin: BrowserWindow;
55 |
56 | function makeSettingsListenerHelpers(o: SettingsStore) {
57 | const listeners = new Map<(data: any) => void, PropertyKey>();
58 |
59 | const addListener: typeof o.addChangeListener = (path, cb) => {
60 | listeners.set(cb, path);
61 | o.addChangeListener(path, cb);
62 | };
63 | const removeAllListeners = () => {
64 | for (const [listener, path] of listeners) {
65 | o.removeChangeListener(path as any, listener);
66 | }
67 |
68 | listeners.clear();
69 | };
70 |
71 | return [addListener, removeAllListeners] as const;
72 | }
73 |
74 | const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings);
75 | const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
76 |
77 | function initTray(win: BrowserWindow) {
78 | const onTrayClick = () => {
79 | if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
80 | else win.show();
81 | };
82 | const trayMenu = Menu.buildFromTemplate([
83 | {
84 | label: "Open",
85 | click() {
86 | win.show();
87 | }
88 | },
89 | {
90 | label: "About",
91 | click: createAboutWindow
92 | },
93 | {
94 | label: "Repair Suncord",
95 | async click() {
96 | await downloadVencordFiles();
97 | app.relaunch();
98 | app.quit();
99 | }
100 | },
101 | {
102 | label: "Reset Sunroof",
103 | async click() {
104 | await clearData(win);
105 | }
106 | },
107 | {
108 | type: "separator"
109 | },
110 | {
111 | label: "Restart",
112 | click() {
113 | app.relaunch();
114 | app.quit();
115 | }
116 | },
117 | {
118 | label: "Quit",
119 | click() {
120 | isQuitting = true;
121 | app.quit();
122 | }
123 | }
124 | ]);
125 |
126 | tray = new Tray(ICON_PATH);
127 | tray.setToolTip("Sunroof");
128 | tray.setContextMenu(trayMenu);
129 | tray.on("click", onTrayClick);
130 | }
131 |
132 | async function clearData(win: BrowserWindow) {
133 | const { response } = await dialog.showMessageBox(win, {
134 | message: "Are you sure you want to reset Sunroof?",
135 | detail: "This will log you out, clear caches and reset all your settings!\n\nSunroof will automatically restart after this operation.",
136 | buttons: ["Yes", "No"],
137 | cancelId: MessageBoxChoice.Cancel,
138 | defaultId: MessageBoxChoice.Default,
139 | type: "warning"
140 | });
141 |
142 | if (response === MessageBoxChoice.Cancel) return;
143 |
144 | win.close();
145 |
146 | await win.webContents.session.clearStorageData();
147 | await win.webContents.session.clearCache();
148 | await win.webContents.session.clearCodeCaches({});
149 | await rm(DATA_DIR, { force: true, recursive: true });
150 |
151 | app.relaunch();
152 | app.quit();
153 | }
154 |
155 | type MenuItemList = Array;
156 |
157 | function initMenuBar(win: BrowserWindow) {
158 | const isWindows = process.platform === "win32";
159 | const isDarwin = process.platform === "darwin";
160 | const wantCtrlQ = !isWindows || VencordSettings.store.winCtrlQ;
161 |
162 | const subMenu = [
163 | {
164 | label: "About Sunroof",
165 | click: createAboutWindow
166 | },
167 | {
168 | label: "Force Update Suncord",
169 | async click() {
170 | await downloadVencordFiles();
171 | app.relaunch();
172 | app.quit();
173 | },
174 | toolTip: "Sunroof will automatically restart after this operation"
175 | },
176 | {
177 | label: "Reset Sunroof",
178 | async click() {
179 | await clearData(win);
180 | },
181 | toolTip: "Sunroof will automatically restart after this operation"
182 | },
183 | {
184 | label: "Relaunch",
185 | accelerator: "CmdOrCtrl+Shift+R",
186 | click() {
187 | app.relaunch();
188 | app.quit();
189 | }
190 | },
191 | ...(!isDarwin
192 | ? []
193 | : ([
194 | {
195 | type: "separator"
196 | },
197 | {
198 | label: "Settings",
199 | accelerator: "CmdOrCtrl+,",
200 | async click() {
201 | mainWin.webContents.executeJavaScript(
202 | "Vencord.Webpack.Common.SettingsRouter.open('My Account')"
203 | );
204 | }
205 | },
206 | {
207 | type: "separator"
208 | },
209 | {
210 | role: "hide"
211 | },
212 | {
213 | role: "hideOthers"
214 | },
215 | {
216 | role: "unhide"
217 | },
218 | {
219 | type: "separator"
220 | }
221 | ] satisfies MenuItemList)),
222 | {
223 | label: "Quit",
224 | accelerator: wantCtrlQ ? "CmdOrCtrl+Q" : void 0,
225 | visible: !isWindows,
226 | role: "quit",
227 | click() {
228 | app.quit();
229 | }
230 | },
231 | isWindows && {
232 | label: "Quit",
233 | accelerator: "Alt+F4",
234 | role: "quit",
235 | click() {
236 | app.quit();
237 | }
238 | },
239 | // See https://github.com/electron/electron/issues/14742 and https://github.com/electron/electron/issues/5256
240 | {
241 | label: "Zoom in (hidden, hack for Qwertz and others)",
242 | accelerator: "CmdOrCtrl+=",
243 | role: "zoomIn",
244 | visible: false
245 | }
246 | ] satisfies MenuItemList;
247 |
248 | const menu = Menu.buildFromTemplate([
249 | {
250 | label: "Sunroof",
251 | role: "appMenu",
252 | submenu: subMenu.filter(isTruthy)
253 | },
254 | { role: "fileMenu" },
255 | { role: "editMenu" },
256 | { role: "viewMenu" },
257 | { role: "windowMenu" }
258 | ]);
259 |
260 | Menu.setApplicationMenu(menu);
261 | }
262 |
263 | function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
264 | // We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized.
265 | if (isDeckGameMode) return {};
266 |
267 | const { x, y, width, height } = State.store.windowBounds ?? {};
268 |
269 | const options = {
270 | width: width ?? DEFAULT_WIDTH,
271 | height: height ?? DEFAULT_HEIGHT
272 | } as BrowserWindowConstructorOptions;
273 |
274 | const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayid);
275 |
276 | if (x != null && y != null && storedDisplay) {
277 | options.x = x;
278 | options.y = y;
279 | }
280 |
281 | if (!Settings.store.disableMinSize) {
282 | options.minWidth = MIN_WIDTH;
283 | options.minHeight = MIN_HEIGHT;
284 | }
285 |
286 | return options;
287 | }
288 |
289 | function getDarwinOptions(): BrowserWindowConstructorOptions {
290 | const options = {
291 | titleBarStyle: "hidden",
292 | trafficLightPosition: { x: 10, y: 10 }
293 | } as BrowserWindowConstructorOptions;
294 |
295 | const { splashTheming, splashBackground } = Settings.store;
296 | const { macosTranslucency } = VencordSettings.store;
297 |
298 | if (macosTranslucency) {
299 | options.vibrancy = "sidebar";
300 | options.backgroundColor = "#ffffff00";
301 | } else {
302 | if (splashTheming) {
303 | options.backgroundColor = splashBackground;
304 | } else {
305 | options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
306 | }
307 | }
308 |
309 | return options;
310 | }
311 |
312 | function initWindowBoundsListeners(win: BrowserWindow) {
313 | const saveState = () => {
314 | State.store.maximized = win.isMaximized();
315 | State.store.minimized = win.isMinimized();
316 | };
317 |
318 | win.on("maximize", saveState);
319 | win.on("minimize", saveState);
320 | win.on("unmaximize", saveState);
321 |
322 | const saveBounds = () => {
323 | State.store.windowBounds = win.getBounds();
324 | State.store.displayid = screen.getDisplayMatching(State.store.windowBounds).id;
325 | };
326 |
327 | win.on("resize", saveBounds);
328 | win.on("move", saveBounds);
329 | }
330 |
331 | function initSettingsListeners(win: BrowserWindow) {
332 | addSettingsListener("tray", enable => {
333 | if (enable) initTray(win);
334 | else tray?.destroy();
335 | });
336 | addSettingsListener("disableMinSize", disable => {
337 | if (disable) {
338 | // 0 no work
339 | win.setMinimumSize(1, 1);
340 | } else {
341 | win.setMinimumSize(MIN_WIDTH, MIN_HEIGHT);
342 |
343 | const { width, height } = win.getBounds();
344 | win.setBounds({
345 | width: Math.max(width, MIN_WIDTH),
346 | height: Math.max(height, MIN_HEIGHT)
347 | });
348 | }
349 | });
350 |
351 | addVencordSettingsListener("macosTranslucency", enabled => {
352 | if (enabled) {
353 | win.setVibrancy("sidebar");
354 | win.setBackgroundColor("#ffffff00");
355 | } else {
356 | win.setVibrancy(null);
357 | win.setBackgroundColor("#ffffff");
358 | }
359 | });
360 |
361 | addSettingsListener("enableMenu", enabled => {
362 | win.setAutoHideMenuBar(enabled ?? false);
363 | });
364 |
365 | addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages));
366 | }
367 |
368 | async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
369 | languages ??= await win.webContents.executeJavaScript("[...new Set(navigator.languages)]").catch(() => []);
370 | if (!languages) return;
371 |
372 | const ses = session.defaultSession;
373 |
374 | const available = ses.availableSpellCheckerLanguages;
375 | const applicable = languages.filter(l => available.includes(l)).slice(0, 5);
376 | if (applicable.length) ses.setSpellCheckerLanguages(applicable);
377 | }
378 |
379 | function initSpellCheck(win: BrowserWindow) {
380 | win.webContents.on("context-menu", (_, data) => {
381 | win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
382 | });
383 |
384 | initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
385 | }
386 |
387 | function createMainWindow() {
388 | // Clear up previous settings listeners
389 | removeSettingsListeners();
390 | removeVencordSettingsListeners();
391 |
392 | const { staticTitle, transparencyOption, enableMenu, customTitleBar } = Settings.store;
393 |
394 | const { frameless, transparent } = VencordSettings.store;
395 |
396 | const noFrame = frameless === true || customTitleBar === true;
397 |
398 | const win = (mainWin = new BrowserWindow({
399 | show: false,
400 | webPreferences: {
401 | nodeIntegration: false,
402 | sandbox: false,
403 | contextIsolation: true,
404 | devTools: true,
405 | preload: join(__dirname, "preload.js"),
406 | spellcheck: true,
407 | // disable renderer backgrounding to prevent the app from unloading when in the background
408 | backgroundThrottling: false
409 | },
410 | icon: ICON_PATH,
411 | frame: !noFrame,
412 | ...(transparent && {
413 | transparent: true,
414 | backgroundColor: "#00000000"
415 | }),
416 | ...(transparencyOption &&
417 | transparencyOption !== "none" && {
418 | backgroundColor: "#00000000",
419 | backgroundMaterial: transparencyOption
420 | }),
421 | // Fix transparencyOption for custom discord titlebar
422 | ...(customTitleBar &&
423 | transparencyOption &&
424 | transparencyOption !== "none" && {
425 | transparent: true
426 | }),
427 | ...(staticTitle && { title: "Sunroof" }),
428 | ...(process.platform === "darwin" && getDarwinOptions()),
429 | ...getWindowBoundsOptions(),
430 | autoHideMenuBar: enableMenu
431 | }));
432 | win.setMenuBarVisibility(false);
433 | if (process.platform === "darwin" && customTitleBar) win.setWindowButtonVisibility(false);
434 |
435 | win.on("close", e => {
436 | const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
437 | if (isQuitting || (process.platform !== "darwin" && !useTray)) return;
438 |
439 | e.preventDefault();
440 |
441 | if (process.platform === "darwin") app.hide();
442 | else win.hide();
443 |
444 | return false;
445 | });
446 |
447 | if (Settings.store.staticTitle) win.on("page-title-updated", e => e.preventDefault());
448 |
449 | initWindowBoundsListeners(win);
450 | if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
451 | initMenuBar(win);
452 | makeLinksOpenExternally(win);
453 | initSettingsListeners(win);
454 | initSpellCheck(win);
455 |
456 | win.webContents.setUserAgent(BrowserUserAgent);
457 |
458 | const subdomain =
459 | Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"
460 | ? `${Settings.store.discordBranch}.`
461 | : "";
462 |
463 | win.loadURL(`https://${subdomain}discord.com/app`);
464 |
465 | return win;
466 | }
467 |
468 | const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
469 |
470 | export async function createWindows() {
471 | const startMinimized = process.argv.includes("--start-minimized");
472 | const splash = createSplashWindow(startMinimized);
473 | // SteamOS letterboxes and scales it terribly, so just full screen it
474 | if (isDeckGameMode) splash.setFullScreen(true);
475 | await ensureVencordFiles();
476 | runVencordMain();
477 |
478 | mainWin = createMainWindow();
479 |
480 | mainWin.webContents.on("did-finish-load", () => {
481 | splash.destroy();
482 |
483 | if (!startMinimized) {
484 | mainWin!.show();
485 | if (State.store.maximized && !isDeckGameMode) mainWin!.maximize();
486 | }
487 |
488 | if (isDeckGameMode) {
489 | // always use entire display
490 | mainWin!.setFullScreen(true);
491 |
492 | askToApplySteamLayout(mainWin);
493 | }
494 |
495 | mainWin.once("show", () => {
496 | if (State.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
497 | mainWin!.maximize();
498 | }
499 | });
500 | });
501 |
502 | initArRPC();
503 | }
504 |
--------------------------------------------------------------------------------
/src/main/mediaPermissions.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { session, systemPreferences } from "electron";
8 |
9 | export function registerMediaPermissionsHandler() {
10 | if (process.platform !== "darwin") return;
11 |
12 | session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
13 | let granted = true;
14 |
15 | if ("mediaTypes" in details) {
16 | if (details.mediaTypes?.includes("audio")) {
17 | granted &&= await systemPreferences.askForMediaAccess("microphone");
18 | }
19 | if (details.mediaTypes?.includes("video")) {
20 | granted &&= await systemPreferences.askForMediaAccess("camera");
21 | }
22 | }
23 |
24 | callback(granted);
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/screenShare.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { desktopCapturer, session, Streams } from "electron";
8 | import type { StreamPick } from "renderer/components/ScreenSharePicker";
9 | import { IpcEvents } from "shared/IpcEvents";
10 |
11 | import { handle } from "./utils/ipcWrappers";
12 |
13 | const isWayland =
14 | process.platform === "linux" && (process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY);
15 |
16 | export function registerScreenShareHandler() {
17 | handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => {
18 | const sources = await desktopCapturer.getSources({
19 | types: ["window", "screen"],
20 | thumbnailSize: {
21 | width: 1920,
22 | height: 1080
23 | }
24 | });
25 | return sources.find(s => s.id === id)?.thumbnail.toDataURL();
26 | });
27 |
28 | session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
29 | // request full resolution on wayland right away because we always only end up with one result anyway
30 | const width = isWayland ? 1920 : 176;
31 | const sources = await desktopCapturer
32 | .getSources({
33 | types: ["window", "screen"],
34 | thumbnailSize: {
35 | width,
36 | height: width * (9 / 16)
37 | }
38 | })
39 | .catch(err => console.error("Error during screenshare picker", err));
40 |
41 | if (!sources) return callback({});
42 |
43 | const data = sources.map(({ id, name, thumbnail }) => ({
44 | id,
45 | name,
46 | url: thumbnail.toDataURL()
47 | }));
48 |
49 | if (isWayland) {
50 | const video = data[0];
51 | if (video) {
52 | const stream = await request.frame
53 | .executeJavaScript(
54 | `Sunroof.Components.ScreenShare.openScreenSharePicker(${JSON.stringify([video])},true)`
55 | )
56 | .catch(() => null);
57 | if (stream === null) return callback({});
58 | }
59 |
60 | callback(video ? { video: sources[0] } : {});
61 | return;
62 | }
63 |
64 | const choice = await request.frame
65 | .executeJavaScript(`Sunroof.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`)
66 | .then(e => e as StreamPick)
67 | .catch(e => {
68 | console.error("Error during screenshare picker", e);
69 | return null;
70 | });
71 |
72 | if (!choice) return callback({});
73 |
74 | const source = sources.find(s => s.id === choice.id);
75 | if (!source) return callback({});
76 |
77 | const streams: Streams = {
78 | video: source
79 | };
80 | if (choice.audio && process.platform === "win32") streams.audio = "loopback";
81 |
82 | callback(streams);
83 | });
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/settings.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8 | import { dirname, join } from "path";
9 | import type { Settings as TSettings, State as TState } from "shared/settings";
10 | import { SettingsStore } from "shared/utils/SettingsStore";
11 |
12 | import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants";
13 |
14 | const SETTINGS_FILE = join(DATA_DIR, "settings.json");
15 | const STATE_FILE = join(DATA_DIR, "state.json");
16 |
17 | function loadSettings(file: string, name: string) {
18 | let settings = {} as T;
19 | try {
20 | const content = readFileSync(file, "utf8");
21 | try {
22 | settings = JSON.parse(content);
23 | } catch (err) {
24 | console.error(`Failed to parse ${name}.json:`, err);
25 | }
26 | } catch {}
27 |
28 | const store = new SettingsStore(settings);
29 | store.addGlobalChangeListener(o => {
30 | mkdirSync(dirname(file), { recursive: true });
31 | writeFileSync(file, JSON.stringify(o, null, 4));
32 | });
33 |
34 | return store;
35 | }
36 |
37 | export const Settings = loadSettings(SETTINGS_FILE, "Sunroof settings");
38 |
39 | export const VencordSettings = loadSettings(VENCORD_SETTINGS_FILE, "Suncord settings");
40 |
41 | if (Object.hasOwn(Settings.plain, "firstLaunch") && !existsSync(STATE_FILE)) {
42 | console.warn("legacy state in settings.json detected. migrating to state.json");
43 | const state = {} as TState;
44 | for (const prop of ["firstLaunch", "maximized", "minimized", "steamOSLayoutVersion", "windowBounds"] as const) {
45 | state[prop] = Settings.plain[prop];
46 | delete Settings.plain[prop];
47 | }
48 | Settings.markAsChanged();
49 | writeFileSync(STATE_FILE, JSON.stringify(state, null, 4));
50 | }
51 |
52 | export const State = loadSettings(STATE_FILE, "Sunroof state");
53 |
--------------------------------------------------------------------------------
/src/main/splash.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { BrowserWindow } from "electron";
8 | import { join } from "path";
9 | import { SplashProps } from "shared/browserWinProperties";
10 | import { ICON_PATH, VIEW_DIR } from "shared/paths";
11 |
12 | import { Settings } from "./settings";
13 |
14 | export function createSplashWindow(startMinimized = false) {
15 | const { splashBackground, splashColor, splashTheming, splashAnimationPath } = Settings.store;
16 |
17 | const splash = new BrowserWindow({
18 | ...SplashProps,
19 | icon: ICON_PATH,
20 | show: !startMinimized
21 | });
22 |
23 | splash.loadFile(join(VIEW_DIR, "splash.html"));
24 |
25 | if (splashTheming) {
26 | if (splashColor) {
27 | const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)");
28 |
29 | splash.webContents.insertCSS(`body { --fg: ${splashColor} !important }`);
30 | splash.webContents.insertCSS(`body { --fg-semi-trans: ${semiTransparentSplashColor} !important }`);
31 | }
32 |
33 | if (splashBackground) {
34 | splash.webContents.insertCSS(`body { --bg: ${splashBackground} !important }`);
35 | }
36 | }
37 |
38 | if (splashAnimationPath) {
39 | splash.webContents.executeJavaScript(`
40 | document.getElementById("animation").src = "splash-animation://img";
41 | `);
42 | } else {
43 | splash.webContents.insertCSS(`img {image-rendering: pixelated}`);
44 | splash.webContents.executeJavaScript(`
45 | document.getElementById("animation").src = "../troll.gif";
46 | `);
47 | }
48 |
49 | return splash;
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/utils/http.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { createWriteStream } from "fs";
8 | import { Readable } from "stream";
9 | import { pipeline } from "stream/promises";
10 | import { setTimeout } from "timers/promises";
11 |
12 | interface FetchieOptions {
13 | retryOnNetworkError?: boolean;
14 | }
15 |
16 | export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) {
17 | const res = await fetchie(url, options, fetchieOpts);
18 | await pipeline(
19 | // @ts-expect-error odd type error
20 | Readable.fromWeb(res.body!),
21 | createWriteStream(file, {
22 | autoClose: true
23 | })
24 | );
25 | }
26 |
27 | const ONE_MINUTE_MS = 1000 * 60;
28 |
29 | export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) {
30 | let res: Response | undefined;
31 |
32 | try {
33 | res = await fetch(url, options);
34 | } catch (err) {
35 | if (retryOnNetworkError) {
36 | console.error("Failed to fetch", url + ".", "Gonna retry with backoff.");
37 |
38 | for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) {
39 | await setTimeout(delayMs);
40 | try {
41 | res = await fetch(url, options);
42 | break;
43 | } catch {}
44 | }
45 | }
46 |
47 | if (!res) throw new Error(`Failed to fetch ${url}\n${err}`);
48 | }
49 |
50 | if (res.ok) return res;
51 |
52 | let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`;
53 |
54 | const reason = await res.text().catch(() => "");
55 | if (reason) msg += `\n${reason}`;
56 |
57 | throw new Error(msg);
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/utils/ipcWrappers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
8 | import { DISCORD_HOSTNAMES } from "main/constants";
9 | import { IpcEvents } from "shared/IpcEvents";
10 |
11 | export function validateSender(frame: WebFrameMain) {
12 | const { hostname, protocol } = new URL(frame.url);
13 | if (protocol === "file:") return;
14 |
15 | if (!DISCORD_HOSTNAMES.includes(hostname)) throw new Error("ipc: Disallowed host " + hostname);
16 | }
17 |
18 | export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
19 | ipcMain.on(event, (e, ...args) => {
20 | validateSender(e.senderFrame);
21 | e.returnValue = cb(e, ...args);
22 | });
23 | }
24 |
25 | export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
26 | ipcMain.handle(event, (e, ...args) => {
27 | validateSender(e.senderFrame);
28 | return cb(e, ...args);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/utils/makeLinksOpenExternally.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { BrowserWindow, shell } from "electron";
8 | import { DISCORD_HOSTNAMES } from "main/constants";
9 |
10 | import { Settings } from "../settings";
11 | import { createOrFocusPopup, setupPopout } from "./popout";
12 | import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS";
13 |
14 | export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } {
15 | if (protocol == null) {
16 | try {
17 | protocol = new URL(url).protocol;
18 | } catch {
19 | return { action: "deny" };
20 | }
21 | }
22 |
23 | switch (protocol) {
24 | case "http:":
25 | case "https:":
26 | if (Settings.store.openLinksWithElectron) {
27 | return { action: "allow" };
28 | }
29 | // eslint-disable-next-line no-fallthrough
30 | case "mailto:":
31 | case "spotify:":
32 | if (isDeckGameMode) {
33 | steamOpenURL(url);
34 | } else {
35 | shell.openExternal(url);
36 | }
37 | break;
38 | case "steam:":
39 | if (isDeckGameMode) {
40 | execSteamURL(url);
41 | } else {
42 | shell.openExternal(url);
43 | }
44 | break;
45 | }
46 |
47 | return { action: "deny" };
48 | }
49 |
50 | export function makeLinksOpenExternally(win: BrowserWindow) {
51 | win.webContents.setWindowOpenHandler(({ url, frameName, features }) => {
52 | try {
53 | var { protocol, hostname, pathname } = new URL(url);
54 | } catch {
55 | return { action: "deny" };
56 | }
57 |
58 | if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) {
59 | return createOrFocusPopup(frameName, features);
60 | }
61 |
62 | if (url === "about:blank" || (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname)))
63 | return { action: "allow" };
64 |
65 | return handleExternalUrl(url, protocol);
66 | });
67 |
68 | win.webContents.on("did-create-window", (win, { frameName }) => {
69 | if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName);
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/utils/popout.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
8 | import { Settings } from "main/settings";
9 |
10 | import { handleExternalUrl } from "./makeLinksOpenExternally";
11 |
12 | const ALLOWED_FEATURES = new Set([
13 | "width",
14 | "height",
15 | "left",
16 | "top",
17 | "resizable",
18 | "movable",
19 | "alwaysOnTop",
20 | "frame",
21 | "transparent",
22 | "hasShadow",
23 | "closable",
24 | "skipTaskbar",
25 | "backgroundColor",
26 | "menubar",
27 | "toolbar",
28 | "location",
29 | "directories",
30 | "titleBarStyle"
31 | ]);
32 |
33 | const MIN_POPOUT_WIDTH = 320;
34 | const MIN_POPOUT_HEIGHT = 180;
35 | const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = {
36 | title: "Discord Popout",
37 | backgroundColor: "#2f3136",
38 | minWidth: MIN_POPOUT_WIDTH,
39 | minHeight: MIN_POPOUT_HEIGHT,
40 | frame: Settings.store.customTitleBar !== true,
41 | titleBarStyle: process.platform === "darwin" ? "hidden" : undefined,
42 | trafficLightPosition:
43 | process.platform === "darwin"
44 | ? {
45 | x: 10,
46 | y: 3
47 | }
48 | : undefined,
49 | webPreferences: {
50 | nodeIntegration: false,
51 | contextIsolation: true
52 | },
53 | autoHideMenuBar: Settings.store.enableMenu
54 | };
55 |
56 | export const PopoutWindows = new Map();
57 |
58 | function focusWindow(window: BrowserWindow) {
59 | window.setAlwaysOnTop(true);
60 | window.focus();
61 | window.setAlwaysOnTop(false);
62 | }
63 |
64 | function parseFeatureValue(feature: string) {
65 | if (feature === "yes") return true;
66 | if (feature === "no") return false;
67 |
68 | const n = Number(feature);
69 | if (!isNaN(n)) return n;
70 |
71 | return feature;
72 | }
73 |
74 | function parseWindowFeatures(features: string) {
75 | const keyValuesParsed = features.split(",");
76 |
77 | return keyValuesParsed.reduce((features, feature) => {
78 | const [key, value] = feature.split("=");
79 | if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value);
80 |
81 | return features;
82 | }, {});
83 | }
84 |
85 | export function createOrFocusPopup(key: string, features: string) {
86 | const existingWindow = PopoutWindows.get(key);
87 | if (existingWindow) {
88 | focusWindow(existingWindow);
89 | return { action: "deny" };
90 | }
91 |
92 | return {
93 | action: "allow",
94 | overrideBrowserWindowOptions: {
95 | ...DEFAULT_POPOUT_OPTIONS,
96 | ...parseWindowFeatures(features)
97 | }
98 | };
99 | }
100 |
101 | export function setupPopout(win: BrowserWindow, key: string) {
102 | win.setMenuBarVisibility(false);
103 |
104 | PopoutWindows.set(key, win);
105 |
106 | /* win.webContents.on("will-navigate", (evt, url) => {
107 | // maybe prevent if not origin match
108 | })*/
109 |
110 | win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url));
111 |
112 | win.once("closed", () => {
113 | win.removeAllListeners();
114 | PopoutWindows.delete(key);
115 | });
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/utils/steamOS.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { BrowserWindow, dialog } from "electron";
8 | import { writeFile } from "fs/promises";
9 | import { join } from "path";
10 |
11 | import { MessageBoxChoice } from "../constants";
12 | import { State } from "../settings";
13 |
14 | // Bump this to re-show the prompt
15 | const layoutVersion = 2;
16 | // Get this from "show details" on the profile after exporting as a shared personal layout or using share with community
17 | const layoutId = "3080264545"; // Vesktop Layout v2
18 | const numberRegex = /^[0-9]*$/;
19 |
20 | let steamPipeQueue = Promise.resolve();
21 |
22 | export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1";
23 |
24 | export function applyDeckKeyboardFix() {
25 | if (!isDeckGameMode) return;
26 | // Prevent constant virtual keyboard spam that eventually crashes Steam.
27 | process.env.GTK_IM_MODULE = "None";
28 | }
29 |
30 | // For some reason SteamAppId is always 0 for non-steam apps so we do this insanity instead.
31 | function getAppId(): string | null {
32 | // /home/deck/.local/share/Steam/steamapps/shadercache/APPID/fozmediav1
33 | const path = process.env.STEAM_COMPAT_MEDIA_PATH;
34 | if (!path) return null;
35 | const pathElems = path?.split("/");
36 | const appId = pathElems[pathElems.length - 2];
37 | if (appId.match(numberRegex)) {
38 | console.log(`Got Steam App ID ${appId}`);
39 | return appId;
40 | }
41 | return null;
42 | }
43 |
44 | export function execSteamURL(url: string) {
45 | // This doesn't allow arbitrary execution despite the weird syntax.
46 | steamPipeQueue = steamPipeQueue.then(() =>
47 | writeFile(
48 | join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"),
49 | // replace ' to prevent argument injection
50 | `'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`,
51 | "utf-8"
52 | )
53 | );
54 | }
55 |
56 | export function steamOpenURL(url: string) {
57 | execSteamURL(`steam://openurl/${url}`);
58 | }
59 |
60 | export async function showGamePage() {
61 | const appId = getAppId();
62 | if (!appId) return;
63 | await execSteamURL(`steam://nav/games/details/${appId}`);
64 | }
65 |
66 | async function showLayout(appId: string) {
67 | execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
68 | }
69 |
70 | export async function askToApplySteamLayout(win: BrowserWindow) {
71 | const appId = getAppId();
72 | if (!appId) return;
73 | if (State.store.steamOSLayoutVersion === layoutVersion) return;
74 | const update = Boolean(State.store.steamOSLayoutVersion);
75 |
76 | // Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed.
77 | const { response } = await dialog.showMessageBox(win, {
78 | message: `${update ? "Update" : "Apply"} Sunroof Steam Input Layout?`,
79 | detail: `Would you like to ${update ? "Update" : "Apply"} Sunroof's recommended Steam Deck controller settings?
80 | ${update ? "Click yes using the touchpad" : "Tap yes"}, then press the X button or tap Apply Layout to confirm.${
81 | update ? " Doing so will undo any customizations you have made." : ""
82 | }
83 | ${update ? "Click" : "Tap"} no to keep your current layout.`,
84 | buttons: ["Yes", "No"],
85 | cancelId: MessageBoxChoice.Cancel,
86 | defaultId: MessageBoxChoice.Default,
87 | type: "question"
88 | });
89 |
90 | if (State.store.steamOSLayoutVersion !== layoutVersion) {
91 | State.store.steamOSLayoutVersion = layoutVersion;
92 | }
93 |
94 | if (response === MessageBoxChoice.Cancel) return;
95 |
96 | await showLayout(appId);
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/utils/vencordLoader.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { mkdirSync } from "fs";
8 | import { access, constants as FsConstants } from "fs/promises";
9 | import { join } from "path";
10 |
11 | import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
12 | import { downloadFile, fetchie } from "./http";
13 |
14 | const API_BASE = "https://api.github.com";
15 |
16 | export const FILES_TO_DOWNLOAD = [
17 | "vencordDesktopMain.js",
18 | "vencordDesktopPreload.js",
19 | "vencordDesktopRenderer.js",
20 | "vencordDesktopRenderer.css"
21 | ];
22 |
23 | export interface ReleaseData {
24 | name: string;
25 | tag_name: string;
26 | html_url: string;
27 | assets: Array<{
28 | name: string;
29 | browser_download_url: string;
30 | }>;
31 | }
32 |
33 | export async function githubGet(endpoint: string) {
34 | const opts: RequestInit = {
35 | headers: {
36 | Accept: "application/vnd.github+json",
37 | "User-Agent": USER_AGENT
38 | }
39 | };
40 |
41 | if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
42 |
43 | return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true });
44 | }
45 |
46 | export async function downloadVencordFiles() {
47 | const release = await githubGet("/repos/verticalsync/Suncord/releases/latest");
48 |
49 | const { assets }: ReleaseData = await release.json();
50 |
51 | await Promise.all(
52 | assets
53 | .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
54 | .map(({ name, browser_download_url }) =>
55 | downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true })
56 | )
57 | );
58 | }
59 |
60 | const existsAsync = (path: string) =>
61 | access(path, FsConstants.F_OK)
62 | .then(() => true)
63 | .catch(() => false);
64 |
65 | export async function isValidVencordInstall(dir: string) {
66 | return Promise.all(FILES_TO_DOWNLOAD.map(f => existsAsync(join(dir, f)))).then(arr => !arr.includes(false));
67 | }
68 |
69 | export async function ensureVencordFiles() {
70 | if (await isValidVencordInstall(VENCORD_FILES_DIR)) return;
71 |
72 | mkdirSync(VENCORD_FILES_DIR, { recursive: true });
73 |
74 | await downloadVencordFiles();
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/venmic.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
8 | import { app, ipcMain } from "electron";
9 | import { join } from "path";
10 | import { IpcEvents } from "shared/IpcEvents";
11 | import { STATIC_DIR } from "shared/paths";
12 |
13 | import { Settings } from "./settings";
14 |
15 | let PatchBay: typeof PatchBayType | undefined;
16 | let patchBayInstance: PatchBayType | undefined;
17 |
18 | let imported = false;
19 | let initialized = false;
20 |
21 | let hasPipewirePulse = false;
22 | let isGlibCxxOutdated = false;
23 |
24 | function importVenmic() {
25 | if (imported) {
26 | return;
27 | }
28 |
29 | imported = true;
30 |
31 | try {
32 | PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic"))
33 | .PatchBay;
34 |
35 | hasPipewirePulse = PatchBay.hasPipeWire();
36 | } catch (e: any) {
37 | console.error("Failed to import venmic", e);
38 | isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
39 | }
40 | }
41 |
42 | function obtainVenmic() {
43 | if (!imported) {
44 | importVenmic();
45 | }
46 |
47 | if (PatchBay && !initialized) {
48 | initialized = true;
49 |
50 | try {
51 | patchBayInstance = new PatchBay();
52 | } catch (e: any) {
53 | console.error("Failed to instantiate venmic", e);
54 | }
55 | }
56 |
57 | return patchBayInstance;
58 | }
59 |
60 | function getRendererAudioServicePid() {
61 | return (
62 | app
63 | .getAppMetrics()
64 | .find(proc => proc.name === "Audio Service")
65 | ?.pid?.toString() ?? "owo"
66 | );
67 | }
68 |
69 | ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
70 | const audioPid = getRendererAudioServicePid();
71 |
72 | const { granularSelect } = Settings.store.audio ?? {};
73 |
74 | const targets = obtainVenmic()
75 | ?.list(granularSelect ? ["application.process.id"] : undefined)
76 | .filter(s => s["application.process.id"] !== audioPid);
77 |
78 | return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
79 | });
80 |
81 | ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
82 | const pid = getRendererAudioServicePid();
83 | const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {};
84 |
85 | const data: LinkData = {
86 | include,
87 | exclude: [{ "application.process.id": pid }],
88 | ignore_devices: ignoreDevices
89 | };
90 |
91 | if (ignoreInputMedia ?? true) {
92 | data.exclude.push({ "media.class": "Stream/Input/Audio" });
93 | }
94 |
95 | if (ignoreVirtual) {
96 | data.exclude.push({ "node.virtual": "true" });
97 | }
98 |
99 | if (workaround) {
100 | data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
101 | }
102 |
103 | return obtainVenmic()?.link(data);
104 | });
105 |
106 | ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => {
107 | const pid = getRendererAudioServicePid();
108 |
109 | const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } =
110 | Settings.store.audio ?? {};
111 |
112 | const data: LinkData = {
113 | include: [],
114 | exclude: [{ "application.process.id": pid }, ...exclude],
115 | only_speakers: onlySpeakers,
116 | ignore_devices: ignoreDevices,
117 | only_default_speakers: onlyDefaultSpeakers
118 | };
119 |
120 | if (ignoreInputMedia ?? true) {
121 | data.exclude.push({ "media.class": "Stream/Input/Audio" });
122 | }
123 |
124 | if (ignoreVirtual) {
125 | data.exclude.push({ "node.virtual": "true" });
126 | }
127 |
128 | if (workaround) {
129 | data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
130 | }
131 |
132 | return obtainVenmic()?.link(data);
133 | });
134 |
135 | ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());
136 |
--------------------------------------------------------------------------------
/src/preload/VesktopNative.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Node } from "@vencord/venmic";
8 | import { ipcRenderer } from "electron";
9 | import type { Settings } from "shared/settings";
10 |
11 | import { IpcEvents } from "../shared/IpcEvents";
12 | import { invoke, sendSync } from "./typedIpc";
13 |
14 | type SpellCheckerResultCallback = (word: string, suggestions: string[]) => void;
15 |
16 | const spellCheckCallbacks = new Set();
17 |
18 | ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => {
19 | spellCheckCallbacks.forEach(cb => cb(w, s));
20 | });
21 |
22 | export const VesktopNative = {
23 | app: {
24 | relaunch: () => invoke(IpcEvents.RELAUNCH),
25 | getVersion: () => sendSync(IpcEvents.GET_VERSION),
26 | setBadgeCount: (count: number) => invoke(IpcEvents.SET_BADGE_COUNT, count),
27 | supportsWindowsTransparency: () => sendSync(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
28 | },
29 | autostart: {
30 | isEnabled: () => sendSync(IpcEvents.AUTOSTART_ENABLED),
31 | enable: () => invoke(IpcEvents.ENABLE_AUTOSTART),
32 | disable: () => invoke(IpcEvents.DISABLE_AUTOSTART)
33 | },
34 | fileManager: {
35 | showItemInFolder: (path: string) => invoke(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
36 | getVencordDir: () => sendSync(IpcEvents.GET_VENCORD_DIR),
37 | selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value),
38 | selectImagePath: () => invoke<"cancelled">(IpcEvents.SELECT_IMAGE_PATH)
39 | },
40 | settings: {
41 | get: () => sendSync(IpcEvents.GET_SETTINGS),
42 | set: (settings: Settings, path?: string) => invoke(IpcEvents.SET_SETTINGS, settings, path)
43 | },
44 | spellcheck: {
45 | getAvailableLanguages: () => sendSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES),
46 | onSpellcheckResult(cb: SpellCheckerResultCallback) {
47 | spellCheckCallbacks.add(cb);
48 | },
49 | offSpellcheckResult(cb: SpellCheckerResultCallback) {
50 | spellCheckCallbacks.delete(cb);
51 | },
52 | replaceMisspelling: (word: string) => invoke(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, word),
53 | addToDictionary: (word: string) => invoke(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word)
54 | },
55 | win: {
56 | focus: () => invoke(IpcEvents.FOCUS),
57 | close: (key?: string) => invoke(IpcEvents.CLOSE, key),
58 | minimize: () => invoke(IpcEvents.MINIMIZE),
59 | maximize: () => invoke(IpcEvents.MAXIMIZE)
60 | },
61 | capturer: {
62 | getLargeThumbnail: (id: string) => invoke(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id)
63 | },
64 | /** only available on Linux. */
65 | virtmic: {
66 | list: () =>
67 | invoke<
68 | { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
69 | >(IpcEvents.VIRT_MIC_LIST),
70 | start: (include: Node[]) => invoke(IpcEvents.VIRT_MIC_START, include),
71 | startSystem: (exclude: Node[]) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
72 | stop: () => invoke(IpcEvents.VIRT_MIC_STOP)
73 | },
74 | arrpc: {
75 | onActivity(cb: (data: string) => void) {
76 | ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data));
77 | }
78 | },
79 | clipboard: {
80 | copyImage: (imageBuffer: Uint8Array, imageSrc: string) =>
81 | invoke(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc)
82 | }
83 | };
84 |
--------------------------------------------------------------------------------
/src/preload/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { contextBridge, ipcRenderer, webFrame } from "electron";
8 | import { readFileSync, watch } from "fs";
9 |
10 | import { IpcEvents } from "../shared/IpcEvents";
11 | import { VesktopNative } from "./VesktopNative";
12 |
13 | contextBridge.exposeInMainWorld("VesktopNative", VesktopNative);
14 |
15 | require(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_PRELOAD_FILE));
16 |
17 | webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT));
18 | webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_RENDERER_SCRIPT));
19 |
20 | // #region css
21 | const rendererCss = ipcRenderer.sendSync(IpcEvents.GET_RENDERER_CSS_FILE);
22 |
23 | const style = document.createElement("style");
24 | style.id = "vcd-css-core";
25 | style.textContent = readFileSync(rendererCss, "utf-8");
26 |
27 | if (document.readyState === "complete") {
28 | document.documentElement.appendChild(style);
29 | } else {
30 | document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), {
31 | once: true
32 | });
33 | }
34 |
35 | if (IS_DEV) {
36 | // persistent means keep process running if watcher is the only thing still running
37 | // which we obviously don't want
38 | watch(rendererCss, { persistent: false }, () => {
39 | document.getElementById("vcd-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
40 | });
41 | }
42 | // #endregion
43 |
--------------------------------------------------------------------------------
/src/preload/typedIpc.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { ipcRenderer } from "electron";
8 | import { IpcEvents } from "shared/IpcEvents";
9 |
10 | export function invoke(event: IpcEvents, ...args: any[]) {
11 | return ipcRenderer.invoke(event, ...args) as Promise;
12 | }
13 |
14 | export function sendSync(event: IpcEvents, ...args: any[]) {
15 | return ipcRenderer.sendSync(event, ...args) as T;
16 | }
17 |
--------------------------------------------------------------------------------
/src/renderer/appBadge.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { filters, waitFor } from "@vencord/types/webpack";
8 | import { RelationshipStore } from "@vencord/types/webpack/common";
9 |
10 | import { Settings } from "./settings";
11 |
12 | let GuildReadStateStore: any;
13 | let NotificationSettingsStore: any;
14 |
15 | export function setBadge() {
16 | if (Settings.store.appBadge === false) return;
17 |
18 | try {
19 | const mentionCount = GuildReadStateStore.getTotalMentionCount();
20 | const pendingRequests = RelationshipStore.getPendingCount();
21 | const hasUnread = GuildReadStateStore.hasAnyUnread();
22 | const disableUnreadBadge = NotificationSettingsStore.getDisableUnreadBadge();
23 |
24 | let totalCount = mentionCount + pendingRequests;
25 | if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1;
26 |
27 | VesktopNative.app.setBadgeCount(totalCount);
28 | } catch (e) {
29 | console.error(e);
30 | }
31 | }
32 |
33 | let toFind = 3;
34 |
35 | function waitForAndSubscribeToStore(name: string, cb?: (m: any) => void) {
36 | waitFor(filters.byStoreName(name), store => {
37 | cb?.(store);
38 | store.addChangeListener(setBadge);
39 |
40 | toFind--;
41 | if (toFind === 0) setBadge();
42 | });
43 | }
44 |
45 | waitForAndSubscribeToStore("GuildReadStateStore", store => (GuildReadStateStore = store));
46 | waitForAndSubscribeToStore("NotificationSettingsStore", store => (NotificationSettingsStore = store));
47 | waitForAndSubscribeToStore("RelationshipStore");
48 |
--------------------------------------------------------------------------------
/src/renderer/components/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | export * as ScreenShare from "./ScreenSharePicker";
8 |
--------------------------------------------------------------------------------
/src/renderer/components/screenSharePicker.css:
--------------------------------------------------------------------------------
1 | .vcd-screen-picker-modal {
2 | padding: 1em;
3 | }
4 |
5 | .vcd-screen-picker-header h1 {
6 | margin: 0;
7 | }
8 |
9 | .vcd-screen-picker-footer {
10 | display: flex;
11 | gap: 1em;
12 | }
13 |
14 | .vcd-screen-picker-card {
15 | flex-grow: 1;
16 | }
17 |
18 | .vcd-screen-picker-grid {
19 | display: grid;
20 | grid-template-columns: 1fr 1fr;
21 | gap: 2em 1em;
22 | }
23 |
24 | .vcd-screen-picker-grid input {
25 | appearance: none;
26 | cursor: pointer;
27 | }
28 |
29 | .vcd-screen-picker-selected img {
30 | border: 2px solid var(--brand-500);
31 | border-radius: 3px;
32 | }
33 |
34 | .vcd-screen-picker-grid label {
35 | overflow: hidden;
36 | padding: 4px 0px;
37 | cursor: pointer;
38 | }
39 |
40 | .vcd-screen-picker-grid label:hover {
41 | outline: 2px solid var(--brand-500);
42 | }
43 |
44 |
45 | .vcd-screen-picker-grid div {
46 | white-space: nowrap;
47 | text-overflow: ellipsis;
48 | overflow: hidden;
49 | text-align: center;
50 | font-weight: 600;
51 | margin-inline: 0.5em;
52 | }
53 |
54 | .vcd-screen-picker-card {
55 | padding: 0.5em;
56 | box-sizing: border-box;
57 | }
58 |
59 | .vcd-screen-picker-preview-img-linux {
60 | width: 60%;
61 | margin-bottom: 0.5em;
62 | }
63 |
64 | .vcd-screen-picker-preview-img {
65 | width: 90%;
66 | margin-bottom: 0.5em;
67 | }
68 |
69 | .vcd-screen-picker-preview {
70 | display: flex;
71 | flex-direction: column;
72 | justify-content: center;
73 | align-items: center;
74 | margin-bottom: 1em;
75 | }
76 |
77 | .vcd-screen-picker-radio input {
78 | display: none;
79 | }
80 |
81 | .vcd-screen-picker-radio {
82 | background-color: var(--background-secondary);
83 | border: 1px solid var(--primary-800);
84 | padding: 0.3em;
85 | cursor: pointer;
86 | }
87 |
88 | .vcd-screen-picker-radio h2 {
89 | margin: 0;
90 | }
91 |
92 | .vcd-screen-picker-radio[data-checked="true"] {
93 | background-color: var(--brand-500);
94 | border-color: var(--brand-500);
95 | }
96 |
97 | .vcd-screen-picker-radio[data-checked="true"] h2 {
98 | color: var(--interactive-active);
99 | }
100 |
101 | .vcd-screen-picker-quality {
102 | display: flex;
103 | gap: 1em;
104 |
105 | margin-bottom: 0.5em;
106 | }
107 |
108 | .vcd-screen-picker-quality section {
109 | flex: 1 1 auto;
110 | }
111 |
112 | .vcd-screen-picker-settings-button {
113 | margin-left: auto;
114 | margin-top: 0.3rem;
115 | }
116 |
117 | .vcd-screen-picker-radios {
118 | display: flex;
119 | width: 100%;
120 | border-radius: 3px;
121 | }
122 |
123 | .vcd-screen-picker-radios label {
124 | flex: 1 1 auto;
125 | text-align: center;
126 | }
127 |
128 | .vcd-screen-picker-radios label:first-child {
129 | border-radius: 3px 0 0 3px;
130 | }
131 |
132 | .vcd-screen-picker-radios label:last-child {
133 | border-radius: 0 3px 3px 0;
134 | }
135 |
136 | .vcd-screen-picker-audio {
137 | margin-bottom: 0;
138 | }
139 |
140 | .vcd-screen-picker-hint-description {
141 | color: var(--header-secondary);
142 | font-size: 14px;
143 | line-height: 20px;
144 | font-weight: 400;
145 | }
146 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/AutoStartToggle.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Switch, useState } from "@vencord/types/webpack/common";
8 |
9 | import { SettingsComponent } from "./Settings";
10 |
11 | export const AutoStartToggle: SettingsComponent = () => {
12 | const [autoStartEnabled, setAutoStartEnabled] = useState(VesktopNative.autostart.isEnabled());
13 |
14 | return (
15 | {
18 | await VesktopNative.autostart[v ? "enable" : "disable"]();
19 | setAutoStartEnabled(v);
20 | }}
21 | note="Automatically start Sunroof on computer start-up"
22 | >
23 | Start With System
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/CustomSplashAnimation.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Button, Forms } from "@vencord/types/webpack/common";
8 |
9 | import { SettingsComponent } from "./Settings";
10 |
11 | export const CustomSplashAnimation: SettingsComponent = ({ settings }) => {
12 | return (
13 | <>
14 |
15 | The animation on the splash window is loaded from{" "}
16 | {settings.splashAnimationPath ? (
17 | {
20 | e.preventDefault();
21 | VesktopNative.fileManager.showItemInFolder(settings.splashAnimationPath!);
22 | }}
23 | >
24 | {settings.splashAnimationPath}
25 |
26 | ) : (
27 | "the default location"
28 | )}
29 |
30 |
31 | {
34 | const choice = await VesktopNative.fileManager.selectImagePath();
35 | if (choice === "cancelled") return;
36 | settings.splashAnimationPath = choice;
37 | }}
38 | >
39 | Change
40 |
41 | (settings.splashAnimationPath = void 0)}
45 | >
46 | Reset
47 |
48 |
49 | >
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/DiscordBranchPicker.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Select } from "@vencord/types/webpack/common";
8 |
9 | import { SettingsComponent } from "./Settings";
10 |
11 | export const DiscordBranchPicker: SettingsComponent = ({ settings }) => {
12 | return (
13 | (settings.discordBranch = v)}
22 | isSelected={v => v === settings.discordBranch}
23 | serialize={s => s}
24 | />
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/NotificationBadgeToggle.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Switch } from "@vencord/types/webpack/common";
8 | import { setBadge } from "renderer/appBadge";
9 |
10 | import { SettingsComponent } from "./Settings";
11 |
12 | export const NotificationBadgeToggle: SettingsComponent = ({ settings }) => {
13 | return (
14 | {
17 | settings.appBadge = v;
18 | if (v) setBadge();
19 | else VesktopNative.app.setBadgeCount(0);
20 | }}
21 | note="Show mention badge on the app icon"
22 | >
23 | Notification Badge
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/Settings.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import "./settings.css";
8 |
9 | import { Forms, Switch, Text } from "@vencord/types/webpack/common";
10 | import { ComponentType } from "react";
11 | import { Settings, useSettings } from "renderer/settings";
12 | import { isMac, isWindows } from "renderer/utils";
13 |
14 | import { AutoStartToggle } from "./AutoStartToggle";
15 | import { CustomSplashAnimation } from "./CustomSplashAnimation";
16 | import { DiscordBranchPicker } from "./DiscordBranchPicker";
17 | import { NotificationBadgeToggle } from "./NotificationBadgeToggle";
18 | import { VencordLocationPicker } from "./VencordLocationPicker";
19 | import { WindowsTransparencyControls } from "./WindowsTransparencyControls";
20 |
21 | interface BooleanSetting {
22 | key: keyof typeof Settings.store;
23 | title: string;
24 | description: string;
25 | defaultValue: boolean;
26 | disabled?(): boolean;
27 | invisible?(): boolean;
28 | }
29 |
30 | export type SettingsComponent = ComponentType<{ settings: typeof Settings.store }>;
31 |
32 | const SettingsOptions: Record> = {
33 | "Discord Branch": [DiscordBranchPicker],
34 | "System Startup & Performance": [
35 | AutoStartToggle,
36 | {
37 | key: "hardwareAcceleration",
38 | title: "Hardware Acceleration",
39 | description: "Enable hardware acceleration",
40 | defaultValue: true
41 | }
42 | ],
43 | "User Interface": [
44 | {
45 | key: "customTitleBar",
46 | title: "Discord Titlebar",
47 | description: "Use Discord's custom title bar instead of the native system one. Requires a full restart.",
48 | defaultValue: isWindows
49 | },
50 | {
51 | key: "staticTitle",
52 | title: "Static Title",
53 | description: 'Makes the window title "Sunroof" instead of changing to the current page',
54 | defaultValue: false
55 | },
56 | {
57 | key: "enableMenu",
58 | title: "Enable Menu Bar",
59 | description: "Enables the application menu bar. Press ALT to toggle visibility.",
60 | defaultValue: false,
61 | disabled: () => Settings.store.customTitleBar ?? isWindows
62 | },
63 | {
64 | key: "splashTheming",
65 | title: "Splash theming",
66 | description: "Adapt the splash window colors to your custom theme",
67 | defaultValue: false
68 | },
69 | WindowsTransparencyControls
70 | ],
71 | Behaviour: [
72 | {
73 | key: "tray",
74 | title: "Tray Icon",
75 | description: "Add a tray icon for Sunroof",
76 | defaultValue: true,
77 | invisible: () => isMac
78 | },
79 | {
80 | key: "minimizeToTray",
81 | title: "Minimize to tray",
82 | description: "Hitting X will make Sunroof minimize to the tray instead of closing",
83 | defaultValue: true,
84 | invisible: () => isMac,
85 | disabled: () => Settings.store.tray === false
86 | },
87 | {
88 | key: "clickTrayToShowHide",
89 | title: "Hide/Show on tray click",
90 | description: "Left clicking tray icon will toggle the sunroof window visibility.",
91 | defaultValue: false
92 | },
93 | {
94 | key: "disableMinSize",
95 | title: "Disable minimum window size",
96 | description: "Allows you to make the window as small as your heart desires",
97 | defaultValue: false
98 | },
99 | {
100 | key: "disableSmoothScroll",
101 | title: "Disable smooth scrolling",
102 | description: "Disables smooth scrolling",
103 | defaultValue: false
104 | }
105 | ],
106 | Notifications: [NotificationBadgeToggle],
107 | Miscelleanous: [
108 | {
109 | key: "arRPC",
110 | title: "Rich Presence",
111 | description: "Enables Rich Presence via arRPC",
112 | defaultValue: false
113 | },
114 |
115 | {
116 | key: "openLinksWithElectron",
117 | title: "Open Links in app (experimental)",
118 | description: "Opens links in a new Sunroof window instead of your web browser",
119 | defaultValue: false
120 | }
121 | ],
122 | "Custom Splash Animation": [CustomSplashAnimation],
123 | "Suncord Location": [VencordLocationPicker]
124 | };
125 |
126 | function SettingsSections() {
127 | const Settings = useSettings();
128 |
129 | const sections = Object.entries(SettingsOptions).map(([title, settings]) => (
130 |
136 | {settings.map(Setting => {
137 | if (typeof Setting === "function") return ;
138 |
139 | const { defaultValue, title, description, key, disabled, invisible } = Setting;
140 | if (invisible?.()) return null;
141 |
142 | return (
143 | (Settings[key as any] = v)}
146 | note={description}
147 | disabled={disabled?.()}
148 | key={key}
149 | >
150 | {title}
151 |
152 | );
153 | })}
154 |
155 | ));
156 |
157 | return <>{sections}>;
158 | }
159 |
160 | export default function SettingsUi() {
161 | return (
162 |
163 |
164 | Sunroof Settings
165 |
166 |
167 |
168 |
169 | );
170 | }
171 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/VencordLocationPicker.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { useForceUpdater } from "@vencord/types/utils";
8 | import { Button, Forms, Toasts } from "@vencord/types/webpack/common";
9 |
10 | import { SettingsComponent } from "./Settings";
11 |
12 | export const VencordLocationPicker: SettingsComponent = ({ settings }) => {
13 | const forceUpdate = useForceUpdater();
14 | const vencordDir = VesktopNative.fileManager.getVencordDir();
15 |
16 | return (
17 | <>
18 |
19 | Suncord files are loaded from{" "}
20 | {vencordDir ? (
21 | {
24 | e.preventDefault();
25 | VesktopNative.fileManager.showItemInFolder(vencordDir!);
26 | }}
27 | >
28 | {vencordDir}
29 |
30 | ) : (
31 | "the default location"
32 | )}
33 |
34 |
35 | {
38 | const choice = await VesktopNative.fileManager.selectVencordDir();
39 | switch (choice) {
40 | case "cancelled":
41 | break;
42 | case "ok":
43 | Toasts.show({
44 | message: "Suncord install changed. Fully restart Sunroof to apply.",
45 | id: Toasts.genId(),
46 | type: Toasts.Type.SUCCESS
47 | });
48 | break;
49 | case "invalid":
50 | Toasts.show({
51 | message:
52 | "You did not choose a valid Suncord install. Make sure you're selecting the dist dir!",
53 | id: Toasts.genId(),
54 | type: Toasts.Type.FAILURE
55 | });
56 | break;
57 | }
58 | forceUpdate();
59 | }}
60 | >
61 | Change
62 |
63 | {
67 | await VesktopNative.fileManager.selectVencordDir(null);
68 | forceUpdate();
69 | }}
70 | >
71 | Reset
72 |
73 |
74 | >
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/WindowsTransparencyControls.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Margins } from "@vencord/types/utils";
8 | import { Forms, Select } from "@vencord/types/webpack/common";
9 |
10 | import { SettingsComponent } from "./Settings";
11 |
12 | export const WindowsTransparencyControls: SettingsComponent = ({ settings }) => {
13 | if (!VesktopNative.app.supportsWindowsTransparency()) return null;
14 |
15 | return (
16 | <>
17 | Transparency Options
18 |
19 | Requires a full restart. You will need a theme that supports transparency for this to work.
20 |
21 |
22 | (settings.transparencyOption = v)}
42 | isSelected={v => v === settings.transparencyOption}
43 | serialize={s => s}
44 | />
45 |
46 |
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/renderer/components/settings/settings.css:
--------------------------------------------------------------------------------
1 | .vcd-location-btns {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | gap: 0.5em;
5 | margin-top: 0.5em;
6 | }
7 |
8 | .vcd-settings-section {
9 | margin-top: 1.5rem;
10 | }
11 |
12 | .vcd-settings-title {
13 | margin-bottom: 0.5rem;
14 | }
--------------------------------------------------------------------------------
/src/renderer/fixes.css:
--------------------------------------------------------------------------------
1 | /* Download Desktop button in guilds list */
2 | [class^=listItem_]:has([data-list-item-id=guildsnav___app-download-button]),
3 | [class^=listItem_]:has(+ [class^=listItem_] [data-list-item-id=guildsnav___app-download-button]) {
4 | display: none;
5 | }
6 |
7 | /* FIXME: remove this once Discord fixes their css to not explode scrollbars on chromium >=121 */
8 | * {
9 | scrollbar-width: unset !important;
10 | scrollbar-color: unset !important;
11 | }
12 |
13 | /* Workaround for making things in the draggable area clickable again on macOS */
14 | .platform-osx [class*=topic_], .platform-osx [class*=notice_] button {
15 | -webkit-app-region: no-drag;
16 | }
17 |
--------------------------------------------------------------------------------
/src/renderer/fixes.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import "./fixes.css";
8 |
9 | import { isWindows, localStorage } from "./utils";
10 |
11 | // Make clicking Notifications focus the window
12 | const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!;
13 | Object.defineProperty(Notification.prototype, "onclick", {
14 | set(onClick) {
15 | originalSetOnClick.call(this, function (this: unknown) {
16 | onClick.apply(this, arguments);
17 | VesktopNative.win.focus();
18 | });
19 | },
20 | configurable: true
21 | });
22 |
23 | // Hide "Download Discord Desktop now!!!!" banner
24 | localStorage.setItem("hideNag", "true");
25 |
26 | // FIXME: Remove eventually.
27 | // Originally, Vencord always used a Windows user agent. This seems to cause captchas
28 | // Now, we use a platform specific UA - HOWEVER, discord FOR SOME REASON????? caches
29 | // device props in localStorage. This code fixes their cache to properly update the platform in SuperProps
30 | if (!isWindows)
31 | try {
32 | const deviceProperties = localStorage.getItem("deviceProperties");
33 | if (deviceProperties && JSON.parse(deviceProperties).os === "Windows")
34 | localStorage.removeItem("deviceProperties");
35 | } catch {}
36 |
--------------------------------------------------------------------------------
/src/renderer/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import "./fixes";
8 | import "./appBadge";
9 | import "./patches";
10 | import "./themedSplash";
11 |
12 | console.log("read if cute :3");
13 |
14 | export * as Components from "./components";
15 | import { findByPropsLazy, onceReady } from "@vencord/types/webpack";
16 | import { Alerts, FluxDispatcher } from "@vencord/types/webpack/common";
17 |
18 | import SettingsUi from "./components/settings/Settings";
19 | import { Settings } from "./settings";
20 | export { Settings };
21 |
22 | const InviteActions = findByPropsLazy("resolveInvite");
23 |
24 | export async function openInviteModal(code: string) {
25 | const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
26 | if (!invite) return false;
27 |
28 | VesktopNative.win.focus();
29 |
30 | FluxDispatcher.dispatch({
31 | type: "INVITE_MODAL_OPEN",
32 | invite,
33 | code,
34 | context: "APP"
35 | });
36 |
37 | return true;
38 | }
39 |
40 | const customSettingsSections = (
41 | Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record) => any)[] }
42 | ).customSections;
43 |
44 | customSettingsSections.push(() => ({
45 | section: "Sunroof",
46 | label: "Sunroof Settings",
47 | element: SettingsUi,
48 | className: "vc-vesktop-settings"
49 | }));
50 |
51 | const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as {
52 | handleEvent(e: MessageEvent): void;
53 | };
54 |
55 | VesktopNative.arrpc.onActivity(async data => {
56 | if (!Settings.store.arRPC) return;
57 |
58 | await onceReady;
59 |
60 | arRPC.handleEvent(new MessageEvent("message", { data }));
61 | });
62 |
63 | // TODO: remove soon
64 | const vencordDir = "vencordDir" as keyof typeof Settings.store;
65 | if (Settings.store[vencordDir]) {
66 | onceReady.then(() =>
67 | setTimeout(
68 | () =>
69 | Alerts.show({
70 | title: "Custom Suncord Location",
71 | body: "Due to security hardening changes in Sunroof, your custom Suncord location had to be reset. Please configure it again in the settings.",
72 | onConfirm: () => delete Settings.store[vencordDir]
73 | }),
74 | 5000
75 | )
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/renderer/patches/enableNotificationsByDefault.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { addPatch } from "./shared";
8 |
9 | addPatch({
10 | patches: [
11 | {
12 | find: '"NotificationSettingsStore',
13 | replacement: {
14 | // FIXME: fix eslint rule
15 | // eslint-disable-next-line no-useless-escape
16 | match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g,
17 | replace: "$&||true"
18 | }
19 | }
20 | ]
21 | });
22 |
--------------------------------------------------------------------------------
/src/renderer/patches/hideSwitchDevice.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { addPatch } from "./shared";
8 |
9 | addPatch({
10 | patches: [
11 | {
12 | find: "lastOutputSystemDevice.justChanged",
13 | replacement: {
14 | // eslint-disable-next-line no-useless-escape
15 | match: /(\i)\.\i\.getState\(\).neverShowModal/,
16 | replace: "$& || $self.shouldIgnore($1)"
17 | }
18 | }
19 | ],
20 |
21 | shouldIgnore(state: any) {
22 | return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share";
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/renderer/patches/hideVenmicInput.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { addPatch } from "./shared";
8 |
9 | addPatch({
10 | patches: [
11 | {
12 | find: 'setSinkId"in',
13 | replacement: {
14 | // eslint-disable-next-line no-useless-escape
15 | match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/,
16 | replace: "return $1 ? $self.filteredDevices"
17 | }
18 | }
19 | ],
20 |
21 | async filteredDevices() {
22 | const original = await navigator.mediaDevices.enumerateDevices();
23 | return original.filter(x => x.label !== "vencord-screen-share");
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/src/renderer/patches/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | // TODO: Possibly auto generate glob if we have more patches in the future
8 | import "./enableNotificationsByDefault";
9 | import "./platformClass";
10 | import "./hideSwitchDevice";
11 | import "./hideVenmicInput";
12 | import "./screenShareFixes";
13 | import "./spellCheck";
14 | import "./windowsTitleBar";
15 |
--------------------------------------------------------------------------------
/src/renderer/patches/platformClass.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Settings } from "renderer/settings";
8 | import { isMac } from "renderer/utils";
9 |
10 | import { addPatch } from "./shared";
11 |
12 | addPatch({
13 | patches: [
14 | {
15 | find: "platform-web",
16 | replacement: {
17 | // eslint-disable-next-line no-useless-escape
18 | match: /(?<=" platform-overlay"\):)\i/,
19 | replace: "$self.getPlatformClass()"
20 | }
21 | }
22 | ],
23 |
24 | getPlatformClass() {
25 | if (Settings.store.customTitleBar) return "platform-win";
26 | if (isMac) return "platform-osx";
27 | return "platform-web";
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/src/renderer/patches/screenShareFixes.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Logger } from "@vencord/types/utils";
8 | import { currentSettings } from "renderer/components/ScreenSharePicker";
9 | import { isLinux } from "renderer/utils";
10 |
11 | const logger = new Logger("VesktopStreamFixes");
12 |
13 | if (isLinux) {
14 | const original = navigator.mediaDevices.getDisplayMedia;
15 |
16 | async function getVirtmic() {
17 | try {
18 | const devices = await navigator.mediaDevices.enumerateDevices();
19 | const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
20 | return audioDevice?.deviceId;
21 | } catch (error) {
22 | return null;
23 | }
24 | }
25 |
26 | navigator.mediaDevices.getDisplayMedia = async function (opts) {
27 | const stream = await original.call(this, opts);
28 | const id = await getVirtmic();
29 |
30 | const frameRate = Number(currentSettings?.fps);
31 | const height = Number(currentSettings?.resolution);
32 | const width = Math.round(height * (16 / 9));
33 | const track = stream.getVideoTracks()[0];
34 |
35 | track.contentHint = String(currentSettings?.contentHint);
36 |
37 | const constraints = {
38 | ...track.getConstraints(),
39 | frameRate: { min: frameRate, ideal: frameRate },
40 | width: { min: 640, ideal: width, max: width },
41 | height: { min: 480, ideal: height, max: height },
42 | advanced: [{ width: width, height: height }],
43 | resizeMode: "none"
44 | };
45 |
46 | track
47 | .applyConstraints(constraints)
48 | .then(() => {
49 | logger.info("Applied constraints successfully. New constraints: ", track.getConstraints());
50 | })
51 | .catch(e => logger.error("Failed to apply constraints.", e));
52 |
53 | if (id) {
54 | const audio = await navigator.mediaDevices.getUserMedia({
55 | audio: {
56 | deviceId: {
57 | exact: id
58 | },
59 | autoGainControl: false,
60 | echoCancellation: false,
61 | noiseSuppression: false
62 | }
63 | });
64 | audio.getAudioTracks().forEach(t => stream.addTrack(t));
65 | }
66 |
67 | return stream;
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/renderer/patches/shared.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Patch } from "@vencord/types/utils/types";
8 |
9 | window.VCDP = {};
10 |
11 | interface PatchData {
12 | patches: Omit[];
13 | [key: string]: any;
14 | }
15 |
16 | export function addPatch(p: P) {
17 | const { patches, ...globals } = p;
18 |
19 | for (const patch of patches as Patch[]) {
20 | if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement];
21 | for (const r of patch.replacement) {
22 | if (typeof r.replace === "string") r.replace = r.replace.replaceAll("$self", "VCDP");
23 | }
24 |
25 | patch.plugin = "Vesktop";
26 | Vencord.Plugins.patches.push(patch);
27 | }
28 |
29 | Object.assign(VCDP, globals);
30 | }
31 |
--------------------------------------------------------------------------------
/src/renderer/patches/spellCheck.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { addContextMenuPatch } from "@vencord/types/api/ContextMenu";
8 | import { findStoreLazy } from "@vencord/types/webpack";
9 | import { FluxDispatcher, Menu, useMemo, useStateFromStores } from "@vencord/types/webpack/common";
10 | import { useSettings } from "renderer/settings";
11 |
12 | import { addPatch } from "./shared";
13 |
14 | let word: string;
15 | let corrections: string[];
16 |
17 | const SpellCheckStore = findStoreLazy("SpellcheckStore");
18 |
19 | // Make spellcheck suggestions work
20 | addPatch({
21 | patches: [
22 | {
23 | find: ".enableSpellCheck)",
24 | replacement: {
25 | // if (isDesktop) { DiscordNative.onSpellcheck(openMenu(props)) } else { e.preventDefault(); openMenu(props) }
26 | match: /else (.{1,3})\.preventDefault\(\),(.{1,3}\(.{1,3}\))(?<=:(.{1,3})\.enableSpellCheck\).+?)/,
27 | // ... else { $self.onSlateContext(() => openMenu(props)) }
28 | replace: "else {$self.onSlateContext($1, $3?.enableSpellCheck, () => $2)}"
29 | }
30 | }
31 | ],
32 |
33 | onSlateContext(e: MouseEvent, hasSpellcheck: boolean | undefined, openMenu: () => void) {
34 | if (!hasSpellcheck) {
35 | e.preventDefault();
36 | openMenu();
37 | return;
38 | }
39 |
40 | const cb = (w: string, c: string[]) => {
41 | VesktopNative.spellcheck.offSpellcheckResult(cb);
42 | word = w;
43 | corrections = c;
44 | openMenu();
45 | };
46 | VesktopNative.spellcheck.onSpellcheckResult(cb);
47 | }
48 | });
49 |
50 | addContextMenuPatch("textarea-context", children => {
51 | const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled());
52 | const hasCorrections = Boolean(word && corrections?.length);
53 |
54 | const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []);
55 |
56 | const settings = useSettings();
57 | const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]);
58 |
59 | const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some(c => c?.props?.id === "paste"));
60 |
61 | children.splice(
62 | pasteSectionIndex === -1 ? children.length : pasteSectionIndex,
63 | 0,
64 |
65 | {hasCorrections && (
66 | <>
67 | {corrections.map(c => (
68 | VesktopNative.spellcheck.replaceMisspelling(c)}
72 | />
73 | ))}
74 |
75 | VesktopNative.spellcheck.addToDictionary(word)}
79 | />
80 | >
81 | )}
82 |
83 |
84 | {
89 | FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
90 | }}
91 | />
92 |
93 |
94 | {availableLanguages.map(lang => {
95 | const isEnabled = spellCheckLanguages.includes(lang);
96 | return (
97 | = 5}
102 | action={() => {
103 | const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang);
104 | if (newSpellCheckLanguages.length === spellCheckLanguages.length) {
105 | newSpellCheckLanguages.push(lang);
106 | }
107 |
108 | settings.spellCheckLanguages = newSpellCheckLanguages;
109 | }}
110 | />
111 | );
112 | })}
113 |
114 |
115 |
116 | );
117 | });
118 |
--------------------------------------------------------------------------------
/src/renderer/patches/windowsTitleBar.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Settings } from "renderer/settings";
8 |
9 | import { addPatch } from "./shared";
10 |
11 | if (Settings.store.customTitleBar)
12 | addPatch({
13 | patches: [
14 | {
15 | find: ".wordmarkWindows",
16 | replacement: [
17 | {
18 | // TODO: Fix eslint rule
19 | // eslint-disable-next-line no-useless-escape
20 | match: /case \i\.\i\.WINDOWS:/,
21 | replace: 'case "WEB":'
22 | },
23 | ...["close", "minimize", "maximize"].map(op => ({
24 | match: new RegExp(String.raw`\i\.\i\.${op}\b`),
25 | replace: `VesktopNative.win.${op}`
26 | }))
27 | ]
28 | }
29 | ]
30 | });
31 |
--------------------------------------------------------------------------------
/src/renderer/settings.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { useEffect, useReducer } from "@vencord/types/webpack/common";
8 | import { SettingsStore } from "shared/utils/SettingsStore";
9 |
10 | export const Settings = new SettingsStore(VesktopNative.settings.get());
11 | Settings.addGlobalChangeListener((o, p) => VesktopNative.settings.set(o, p));
12 |
13 | export function useSettings() {
14 | const [, update] = useReducer(x => x + 1, 0);
15 |
16 | useEffect(() => {
17 | Settings.addGlobalChangeListener(update);
18 |
19 | return () => Settings.removeGlobalChangeListener(update);
20 | }, []);
21 |
22 | return Settings.store;
23 | }
24 |
25 | export function getValueAndOnChange(key: keyof typeof Settings.store) {
26 | return {
27 | value: Settings.store[key] as any,
28 | onChange: (value: any) => (Settings.store[key] = value)
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/src/renderer/themedSplash.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { Settings } from "./settings";
8 |
9 | function isValidColor(color: CSSStyleValue | undefined): color is CSSUnparsedValue & { [0]: string } {
10 | return color instanceof CSSUnparsedValue && typeof color[0] === "string" && CSS.supports("color", color[0]);
11 | }
12 |
13 | function resolveColor(color: string) {
14 | const span = document.createElement("span");
15 | span.style.color = color;
16 | span.style.display = "none";
17 |
18 | document.body.append(span);
19 | const rgbColor = getComputedStyle(span).color;
20 | span.remove();
21 |
22 | return rgbColor;
23 | }
24 |
25 | const updateSplashColors = () => {
26 | const bodyStyles = document.body.computedStyleMap();
27 |
28 | const color = bodyStyles.get("--text-normal");
29 | const backgroundColor = bodyStyles.get("--background-primary");
30 |
31 | if (isValidColor(color)) {
32 | Settings.store.splashColor = resolveColor(color[0]);
33 | }
34 |
35 | if (isValidColor(backgroundColor)) {
36 | Settings.store.splashBackground = resolveColor(backgroundColor[0]);
37 | }
38 | };
39 |
40 | if (document.readyState === "complete") {
41 | updateSplashColors();
42 | } else {
43 | window.addEventListener("load", updateSplashColors);
44 | }
45 |
46 | window.addEventListener("beforeunload", updateSplashColors);
47 |
--------------------------------------------------------------------------------
/src/renderer/utils.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | export const { localStorage } = window;
8 |
9 | export const isFirstRun = (() => {
10 | const key = "VCD_FIRST_RUN";
11 | if (localStorage.getItem(key) !== null) return false;
12 | localStorage.setItem(key, "false");
13 | return true;
14 | })();
15 |
16 | const { platform } = navigator;
17 |
18 | export const isWindows = platform.startsWith("Win");
19 | export const isMac = platform.startsWith("Mac");
20 | export const isLinux = platform.startsWith("Linux");
21 |
--------------------------------------------------------------------------------
/src/shared/IpcEvents.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | export const enum IpcEvents {
8 | GET_VENCORD_PRELOAD_FILE = "VCD_GET_VC_PRELOAD_FILE",
9 | GET_VENCORD_RENDERER_SCRIPT = "VCD_GET_VC_RENDERER_SCRIPT",
10 | GET_RENDERER_SCRIPT = "VCD_GET_RENDERER_SCRIPT",
11 | GET_RENDERER_CSS_FILE = "VCD_GET_RENDERER_CSS_FILE",
12 |
13 | GET_VERSION = "VCD_GET_VERSION",
14 | SUPPORTS_WINDOWS_TRANSPARENCY = "VCD_SUPPORTS_WINDOWS_TRANSPARENCY",
15 |
16 | RELAUNCH = "VCD_RELAUNCH",
17 | CLOSE = "VCD_CLOSE",
18 | FOCUS = "VCD_FOCUS",
19 | MINIMIZE = "VCD_MINIMIZE",
20 | MAXIMIZE = "VCD_MAXIMIZE",
21 |
22 | SHOW_ITEM_IN_FOLDER = "VCD_SHOW_ITEM_IN_FOLDER",
23 | GET_SETTINGS = "VCD_GET_SETTINGS",
24 | SET_SETTINGS = "VCD_SET_SETTINGS",
25 |
26 | GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR",
27 | SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",
28 | SELECT_IMAGE_PATH = "VCD_SELECT_IMAGE_PATH",
29 |
30 | UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA",
31 | UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
32 | UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
33 |
34 | SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
35 | SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
36 | SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING",
37 | SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",
38 |
39 | SET_BADGE_COUNT = "VCD_SET_BADGE_COUNT",
40 |
41 | CAPTURER_GET_LARGE_THUMBNAIL = "VCD_CAPTURER_GET_LARGE_THUMBNAIL",
42 |
43 | AUTOSTART_ENABLED = "VCD_AUTOSTART_ENABLED",
44 | ENABLE_AUTOSTART = "VCD_ENABLE_AUTOSTART",
45 | DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART",
46 |
47 | VIRT_MIC_LIST = "VCD_VIRT_MIC_LIST",
48 | VIRT_MIC_START = "VCD_VIRT_MIC_START",
49 | VIRT_MIC_START_SYSTEM = "VCD_VIRT_MIC_START_ALL",
50 | VIRT_MIC_STOP = "VCD_VIRT_MIC_STOP",
51 |
52 | ARRPC_ACTIVITY = "VCD_ARRPC_ACTIVITY",
53 |
54 | CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE"
55 | }
56 |
--------------------------------------------------------------------------------
/src/shared/browserWinProperties.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import type { BrowserWindowConstructorOptions } from "electron";
8 |
9 | export const SplashProps: BrowserWindowConstructorOptions = {
10 | transparent: true,
11 | frame: false,
12 | height: 350,
13 | width: 300,
14 | center: true,
15 | resizable: false,
16 | maximizable: false,
17 | alwaysOnTop: true
18 | };
19 |
--------------------------------------------------------------------------------
/src/shared/paths.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { join } from "path";
8 |
9 | export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
10 | export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views");
11 | export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");
12 | export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png");
13 |
--------------------------------------------------------------------------------
/src/shared/settings.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import type { Rectangle } from "electron";
8 |
9 | export interface Settings {
10 | discordBranch?: "stable" | "canary" | "ptb";
11 | transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
12 | tray?: boolean;
13 | minimizeToTray?: boolean;
14 | openLinksWithElectron?: boolean;
15 | staticTitle?: boolean;
16 | enableMenu?: boolean;
17 | disableSmoothScroll?: boolean;
18 | hardwareAcceleration?: boolean;
19 | arRPC?: boolean;
20 | appBadge?: boolean;
21 | disableMinSize?: boolean;
22 | clickTrayToShowHide?: boolean;
23 | customTitleBar?: boolean;
24 |
25 | splashTheming?: boolean;
26 | splashColor?: string;
27 | splashAnimationPath?: string;
28 | splashBackground?: string;
29 |
30 | spellCheckLanguages?: string[];
31 |
32 | audio?: {
33 | workaround?: boolean;
34 | granularSelect?: boolean;
35 |
36 | ignoreVirtual?: boolean;
37 | ignoreDevices?: boolean;
38 | ignoreInputMedia?: boolean;
39 |
40 | onlySpeakers?: boolean;
41 | onlyDefaultSpeakers?: boolean;
42 | };
43 | }
44 |
45 | export interface State {
46 | maximized?: boolean;
47 | minimized?: boolean;
48 | windowBounds?: Rectangle;
49 | displayid: int;
50 |
51 | firstLaunch?: boolean;
52 |
53 | steamOSLayoutVersion?: number;
54 |
55 | vencordDir?: string;
56 | }
57 |
--------------------------------------------------------------------------------
/src/shared/utils/SettingsStore.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | import { LiteralUnion } from "type-fest";
8 |
9 | // Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
10 | type ResolvePropDeep = P extends `${infer Pre}.${infer Suf}`
11 | ? Pre extends keyof T
12 | ? ResolvePropDeep
13 | : any
14 | : P extends keyof T
15 | ? T[P]
16 | : any;
17 |
18 | /**
19 | * The SettingsStore allows you to easily create a mutable store that
20 | * has support for global and path-based change listeners.
21 | */
22 | export class SettingsStore {
23 | private pathListeners = new Map void>>();
24 | private globalListeners = new Set<(newData: T, path: string) => void>();
25 |
26 | /**
27 | * The store object. Making changes to this object will trigger the applicable change listeners
28 | */
29 | public declare store: T;
30 | /**
31 | * The plain data. Changes to this object will not trigger any change listeners
32 | */
33 | public declare plain: T;
34 |
35 | public constructor(plain: T) {
36 | this.plain = plain;
37 | this.store = this.makeProxy(plain);
38 | }
39 |
40 | private makeProxy(object: any, root: T = object, path: string = "") {
41 | const self = this;
42 |
43 | return new Proxy(object, {
44 | get(target, key: string) {
45 | const v = target[key];
46 |
47 | if (typeof v === "object" && v !== null && !Array.isArray(v))
48 | return self.makeProxy(v, root, `${path}${path && "."}${key}`);
49 |
50 | return v;
51 | },
52 | set(target, key: string, value) {
53 | if (target[key] === value) return true;
54 |
55 | Reflect.set(target, key, value);
56 | const setPath = `${path}${path && "."}${key}`;
57 |
58 | self.globalListeners.forEach(cb => cb(root, setPath));
59 | self.pathListeners.get(setPath)?.forEach(cb => cb(value));
60 |
61 | return true;
62 | },
63 | deleteProperty(target, key: string) {
64 | if (!(key in target)) return true;
65 |
66 | const res = Reflect.deleteProperty(target, key);
67 | if (!res) return false;
68 |
69 | const setPath = `${path}${path && "."}${key}`;
70 |
71 | self.globalListeners.forEach(cb => cb(root, setPath));
72 | self.pathListeners.get(setPath)?.forEach(cb => cb(undefined));
73 |
74 | return res;
75 | }
76 | });
77 | }
78 |
79 | /**
80 | * Set the data of the store.
81 | * This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables)
82 | *
83 | * Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data
84 | * @param value New data
85 | * @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc
86 | */
87 | public setData(value: T, pathToNotify?: string) {
88 | this.plain = value;
89 | this.store = this.makeProxy(value);
90 |
91 | if (pathToNotify) {
92 | let v = value;
93 |
94 | const path = pathToNotify.split(".");
95 | for (const p of path) {
96 | if (!v) {
97 | console.warn(
98 | `Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update`
99 | );
100 | return;
101 | }
102 | v = v[p];
103 | }
104 |
105 | this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v));
106 | }
107 |
108 | this.globalListeners.forEach(cb => cb(value, ""));
109 | }
110 |
111 | /**
112 | * Add a global change listener, that will fire whenever any setting is changed
113 | */
114 | public addGlobalChangeListener(cb: (data: T, path: string) => void) {
115 | this.globalListeners.add(cb);
116 | }
117 |
118 | /**
119 | * Add a scoped change listener that will fire whenever a setting matching the specified path is changed.
120 | *
121 | * For example if path is `"foo.bar"`, the listener will fire on
122 | * ```js
123 | * Setting.store.foo.bar = "hi"
124 | * ```
125 | * but not on
126 | * ```js
127 | * Setting.store.foo.baz = "hi"
128 | * ```
129 | * @param path
130 | * @param cb
131 | */
132 | public addChangeListener>(
133 | path: P,
134 | cb: (data: ResolvePropDeep) => void
135 | ) {
136 | const listeners = this.pathListeners.get(path as string) ?? new Set();
137 | listeners.add(cb);
138 | this.pathListeners.set(path as string, listeners);
139 | }
140 |
141 | /**
142 | * Remove a global listener
143 | * @see {@link addGlobalChangeListener}
144 | */
145 | public removeGlobalChangeListener(cb: (data: T, path: string) => void) {
146 | this.globalListeners.delete(cb);
147 | }
148 |
149 | /**
150 | * Remove a scoped listener
151 | * @see {@link addChangeListener}
152 | */
153 | public removeChangeListener(path: LiteralUnion, cb: (data: any) => void) {
154 | const listeners = this.pathListeners.get(path as string);
155 | if (!listeners) return;
156 |
157 | listeners.delete(cb);
158 | if (!listeners.size) this.pathListeners.delete(path as string);
159 | }
160 |
161 | /**
162 | * Call all global change listeners
163 | */
164 | public markAsChanged() {
165 | this.globalListeners.forEach(cb => cb(this.plain, ""));
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/shared/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | /**
8 | * Returns a new function that will only be called after the given delay.
9 | * Subsequent calls will cancel the previous timeout and start a new one from 0
10 | *
11 | * Useful for grouping multiple calls into one
12 | */
13 | export function debounce(func: T, delay = 300): T {
14 | let timeout: NodeJS.Timeout;
15 | return function (...args: any[]) {
16 | clearTimeout(timeout);
17 | timeout = setTimeout(() => {
18 | func(...args);
19 | }, delay);
20 | } as any;
21 | }
22 |
--------------------------------------------------------------------------------
/src/shared/utils/guards.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | export function isTruthy(item: T): item is Exclude {
8 | return Boolean(item);
9 | }
10 |
11 | export function isNonNullish(item: T): item is Exclude {
12 | return item != null;
13 | }
14 |
--------------------------------------------------------------------------------
/src/shared/utils/once.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | /**
8 | * Wraps the given function so that it can only be called once
9 | * @param fn Function to wrap
10 | * @returns New function that can only be called once
11 | */
12 | export function once(fn: T): T {
13 | let called = false;
14 | return function (this: any, ...args: any[]) {
15 | if (called) return;
16 | called = true;
17 | return fn.apply(this, args);
18 | } as any;
19 | }
20 |
--------------------------------------------------------------------------------
/src/shared/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: GPL-3.0
3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience
4 | * Copyright (c) 2023 Vendicated and Vencord contributors
5 | */
6 |
7 | export function sleep(ms: number): Promise {
8 | return new Promise(r => setTimeout(r, ms));
9 | }
10 |
--------------------------------------------------------------------------------
/static/badges/1.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/1.ico
--------------------------------------------------------------------------------
/static/badges/10.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/10.ico
--------------------------------------------------------------------------------
/static/badges/11.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/11.ico
--------------------------------------------------------------------------------
/static/badges/2.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/2.ico
--------------------------------------------------------------------------------
/static/badges/3.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/3.ico
--------------------------------------------------------------------------------
/static/badges/4.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/4.ico
--------------------------------------------------------------------------------
/static/badges/5.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/5.ico
--------------------------------------------------------------------------------
/static/badges/6.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/6.ico
--------------------------------------------------------------------------------
/static/badges/7.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/7.ico
--------------------------------------------------------------------------------
/static/badges/8.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/8.ico
--------------------------------------------------------------------------------
/static/badges/9.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/badges/9.ico
--------------------------------------------------------------------------------
/static/dist/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/static/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/icon.ico
--------------------------------------------------------------------------------
/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/icon.png
--------------------------------------------------------------------------------
/static/troll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verticalsync/Sunroof/20b66803ba20c46da8a6359e72a5b0ff653cf304/static/troll.gif
--------------------------------------------------------------------------------
/static/views/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 | Sunroof
17 |
18 | Sunroof is a free/libre cross platform desktop app aiming to give you a snappier Discord experience with Suncord
19 | pre-installed
20 |
21 |
22 |
36 |
37 |
38 | Acknowledgements
39 | These awesome libraries empower Sunroof
40 |
41 |
42 | Electron
43 | - Build cross-platform desktop apps with JavaScript, HTML, and CSS
44 |
45 |
46 | Electron Builder
47 | - A complete solution to package and build a ready for distribution Electron app with “auto update”
48 | support out of the box
49 |
50 |
51 | arrpc
52 | - An open implementation of Discord's Rich Presence server
53 |
54 |
55 | rohrkabel
56 | - A C++ RAII Pipewire-API Wrapper
57 |
58 |
59 | And many
60 | more awesome open source libraries
63 |
64 |
65 |
66 |
67 |
68 |
76 |
--------------------------------------------------------------------------------
/static/views/first-launch.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
104 |
105 |
106 |
107 | Welcome to Sunroof
108 | Let's customise your experience!
109 |
110 |
155 |
156 | Quit
157 | Submit
158 |
159 |
160 |
161 |
171 |
--------------------------------------------------------------------------------
/static/views/splash.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
Loading Sunroof...
37 |
38 |
39 |
--------------------------------------------------------------------------------
/static/views/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: white;
3 | --fg: black;
4 | --fg-secondary: #313338;
5 | --fg-semi-trans: rgb(0 0 0 / 0.2);
6 | --link: #006ce7;
7 | }
8 |
9 | @media (prefers-color-scheme: dark) {
10 | :root {
11 | --bg: hsl(223 6.7% 20.6%);
12 | --fg: white;
13 | --fg-secondary: #b5bac1;
14 | --fg-semi-trans: rgb(255 255 255 / 0.2);
15 | --link: #00a8fc;
16 | }
17 | }
18 |
19 | body {
20 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
21 | "Open Sans", "Helvetica Neue", sans-serif;
22 | margin: 0;
23 | padding: 0;
24 | background: var(--bg);
25 | color: var(--fg);
26 | }
27 |
28 | a {
29 | color: var(--link);
30 | }
31 |
--------------------------------------------------------------------------------
/static/views/updater.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
49 |
50 |
51 |
52 |
53 | Update Available
54 | There's a new update for Sunroof! Update now to get new fixes and features!
55 |
56 | Current:
57 |
58 | Latest:
59 |
60 |
61 | Changelog
62 | Loading...
63 |
64 |
65 |
76 |
77 |
78 |
79 |
110 |
111 |
124 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "esModuleInterop": true,
5 | "lib": ["DOM", "DOM.Iterable", "esnext", "esnext.array", "esnext.asynciterable", "esnext.symbol"],
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "noImplicitAny": false,
10 | "target": "ESNEXT",
11 | "jsx": "preserve",
12 |
13 | // we have duplicate electron types but it's w/e
14 | "skipLibCheck": true,
15 |
16 | "baseUrl": "./src/",
17 |
18 | "typeRoots": ["./node_modules/@types", "./node_modules/@vencord"]
19 | },
20 | "include": ["src/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------