├── .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] <title>" 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 [<img src="./static/icon.png" width="225" align="right" alt="Sunroof">](https://github.com/verticalsync/Sunroof) 6 | 7 | [![Suncord](https://img.shields.io/badge/Suncord-yellow?style=flat)](https://github.com/verticalsync/Suncord) 8 | [![Tests](https://github.com/verticalsync/Sunroof/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/verticalsync/Sunroof/actions/workflows/test.yml) 9 | [![Discord](https://img.shields.io/discord/1207691698386501634.svg?color=768AD4&label=Discord&logo=discord&logoColor=white)](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.<br></br> 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 | [![Download on Flathub](https://flathub.org/api/badge?svg)](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://raw.githubusercontent.com/verticalsync/github-sponsor-graph/main/graph.png)](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 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 2 | <plist version="1.0"> 3 | <dict> 4 | <key>com.apple.security.cs.allow-unsigned-executable-memory</key> 5 | <true/> 6 | <key>com.apple.security.cs.allow-jit</key> 7 | <true/> 8 | <key>com.apple.security.network.client</key> 9 | <true/> 10 | <key>com.apple.security.device.audio-input</key> 11 | <true/> 12 | <key>com.apple.security.device.camera</key> 13 | <true/> 14 | <key>com.apple.security.device.bluetooth</key> 15 | <true/> 16 | <key>com.apple.security.cs.allow-dyld-environment-variables</key> 17 | <true/> 18 | <key>com.apple.security.cs.disable-library-validation</key> 19 | <true/> 20 | </dict> 21 | </plist> 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 | <?xml version="1.0" encoding="utf-8"?> 2 | <component type="desktop-application"> 3 | <!--Created with jdAppStreamEdit 7.1--> 4 | <id>io.github.verticalsync.sunroof</id> 5 | <name>Sunroof</name> 6 | <summary>Snappier Discord app with Suncord</summary> 7 | <developer_name>Suncord Contributors</developer_name> 8 | <launchable type="desktop-id">io.github.verticalsync.sunroof.desktop</launchable> 9 | <metadata_license>CC0-1.0</metadata_license> 10 | <project_license>GPL-3.0</project_license> 11 | <project_group>Suncord</project_group> 12 | <description> 13 | <p>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.</p> 14 | <p>Which comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.</p> 15 | </description> 16 | <screenshots> 17 | <screenshot type="default"> 18 | <caption>Suncord settings page and about window open</caption> 19 | <image type="source">https://files.catbox.moe/59wp6h.png</image> 20 | </screenshot> 21 | <screenshot> 22 | <caption>A dialog showing screenshare options</caption> 23 | <image type="source">https://files.catbox.moe/nbo64w.png</image> 24 | </screenshot> 25 | <screenshot> 26 | <caption>A screenshot of a Discord server</caption> 27 | <image type="source">https://files.catbox.moe/uzpk4h.png</image> 28 | </screenshot> 29 | </screenshots> 30 | <releases> 31 | <release version="1.6.1" date="2024-07-04" type="stable"> 32 | <url>https://github.com/verticalsync/Sunroof/releases/tag/v1.6.1</url> 33 | <description> 34 | <p>Add new commits from Vesktop</p> 35 | </description> 36 | </release> 37 | <release version="1.6.0" date="2024-03-25" type="stable"> 38 | <url>https://github.com/verticalsync/Sunroof/releases/tag/1.6.0</url> 39 | <description> 40 | <p>Add new commits from Vesktop</p> 41 | </description> 42 | </release> 43 | <release version="1.5.9" date="2024-03-25" type="stable"> 44 | <url>https://github.com/verticalsync/Sunroof/releases/tag/1.5.9</url> 45 | <description> 46 | <p>Re-add support for MacOS</p> 47 | </description> 48 | </release> 49 | <release version="1.5.7" date="2024-03-25" type="stable"> 50 | <url>https://github.com/verticalsync/Sunroof/releases/tag/1.5.7</url> 51 | <description> 52 | <p>New Features</p> 53 | <ul> 54 | <li>Able to change the splash image to anything you want</li> 55 | </ul> 56 | </description> 57 | </release> 58 | <release version="1.5.6" date="2024-02-24" type="stable"> 59 | <url>https://github.com/verticalsync/Sunroof/releases/tag/1.5.6</url> 60 | <description> 61 | <p>Fixes</p> 62 | <ul> 63 | <li>Fix screenshare and some other things not working</li> 64 | </ul> 65 | </description> 66 | </release> 67 | <release version="1.5.5" date="2024-02-24" type="stable"> 68 | <url>https://github.com/verticalsync/Sunroof/releases/tag/1.5.5</url> 69 | <description> 70 | <p>New Features</p> 71 | <ul> 72 | <li>Added categories to Vesktop settings to reduce visual clutter by @justin13888</li> 73 | <li>Added support for Vencord's transparent window options</li> 74 | </ul> 75 | <p>Fixes</p> 76 | <ul> 77 | <li>Fixed ugly error popups when starting Vesktop without working internet connection</li> 78 | <li>Fixed popout title bars on Windows</li> 79 | <li>Fixed spellcheck entries</li> 80 | <li>Fixed screenshare audio using microphone on debian, by @Curve</li> 81 | <li>Fixed a bug where autostart on Linux won't preserve command line flags</li> 82 | </ul> 83 | </description> 84 | </release> 85 | <release version="1.5.0" date="2024-01-16" type="stable"> 86 | <url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.0</url> 87 | <description> 88 | <p>What's Changed</p> 89 | <ul> 90 | <li>fully renamed to Vesktop. You will likely have to login to Discord again. You might have to re-create your vesktop shortcut</li> 91 | <li>added option to disable smooth scrolling by @ZirixCZ</li> 92 | <li>added setting to disable hardware acceleration by @zt64</li> 93 | <li>fixed adding connections</li> 94 | <li>fixed / improved discord popouts</li> 95 | <li>you can now use the custom discord titlebar on linux/mac</li> 96 | <li>the splash window is now draggable</li> 97 | <li>now signed on mac</li> 98 | </ul> 99 | </description> 100 | </release> 101 | <release version="0.4.4" date="2023-12-02" type="stable"> 102 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url> 103 | <description> 104 | <p>What's Changed</p> 105 | <ul> 106 | <li>improve venmic system compatibility by @Curve</li> 107 | <li>Update steamdeck controller layout by @AAGaming00</li> 108 | <li>feat: Add option to disable smooth scrolling by @ZirixCZ</li> 109 | <li>unblur shiggy in splash screen by @viacoro</li> 110 | <li>update electron & arrpc @D3SOX</li> 111 | </ul> 112 | </description> 113 | </release> 114 | <release version="0.4.3" date="2023-11-01" type="stable"> 115 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.3</url> 116 | </release> 117 | <release version="0.4.2" date="2023-10-26" type="stable"> 118 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.2</url> 119 | </release> 120 | <release version="0.4.1" date="2023-10-24" type="stable"> 121 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.1</url> 122 | </release> 123 | <release version="0.4.0" date="2023-10-21" type="stable"> 124 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.0</url> 125 | </release> 126 | <release version="0.3.3" date="2023-09-30" type="stable"> 127 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.3</url> 128 | </release> 129 | <release version="0.3.2" date="2023-09-25" type="stable"> 130 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.2</url> 131 | </release> 132 | <release version="0.3.1" date="2023-09-25" type="stable"> 133 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.1</url> 134 | </release> 135 | <release version="0.3.0" date="2023-08-16" type="stable"> 136 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.0</url> 137 | </release> 138 | <release version="0.2.9" date="2023-08-12" type="stable"> 139 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.9</url> 140 | </release> 141 | <release version="0.2.8" date="2023-08-02" type="stable"> 142 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.8</url> 143 | </release> 144 | <release version="0.2.7" date="2023-07-26" type="stable"> 145 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.7</url> 146 | </release> 147 | <release version="0.2.6" date="2023-07-04" type="stable"> 148 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.6</url> 149 | </release> 150 | <release version="0.2.5" date="2023-06-26" type="stable"> 151 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.5</url> 152 | </release> 153 | <release version="0.2.4" date="2023-06-25" type="stable"> 154 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.4</url> 155 | </release> 156 | <release version="0.2.3" date="2023-06-23" type="stable"> 157 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.3</url> 158 | </release> 159 | <release version="0.2.2" date="2023-06-21" type="stable"> 160 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.2</url> 161 | </release> 162 | <release version="0.2.1" date="2023-06-21" type="stable"> 163 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.1</url> 164 | </release> 165 | <release version="0.2.0" date="2023-05-03" type="stable"> 166 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.0</url> 167 | </release> 168 | <release version="0.1.9" date="2023-04-27" type="stable"> 169 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.9</url> 170 | </release> 171 | <release version="0.1.8" date="2023-04-15" type="stable"> 172 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.8</url> 173 | </release> 174 | <release version="0.1.7" date="2023-04-15" type="stable"> 175 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.7</url> 176 | </release> 177 | <release version="0.1.6" date="2023-04-11" type="stable"> 178 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.6</url> 179 | </release> 180 | <release version="0.1.5" date="2023-04-10" type="stable"> 181 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.5</url> 182 | </release> 183 | <release version="0.1.4" date="2023-04-09" type="stable"> 184 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.4</url> 185 | </release> 186 | <release version="0.1.3" date="2023-04-06" type="stable"> 187 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.3</url> 188 | </release> 189 | <release version="0.1.2" date="2023-04-05" type="stable"> 190 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.2</url> 191 | </release> 192 | <release version="0.1.1" date="2023-04-04" type="stable"> 193 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.1</url> 194 | </release> 195 | <release version="0.1.0" date="2023-04-04" type="development"> 196 | <url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.0</url> 197 | </release> 198 | </releases> 199 | <url type="homepage">https://github.com/verticalsync/Sunroof</url> 200 | <url type="bugtracker">https://github.com/verticalsync/Sunroof/issues</url> 201 | <url type="help">https://github.com/verticalsync/Sunroof/issues</url> 202 | <url type="donation">https://github.com/sponsors/verticalsync</url> 203 | <url type="vcs-browser">https://github.com/verticalsync/Sunroof</url> 204 | <categories> 205 | <category>InstantMessaging</category> 206 | <category>Network</category> 207 | </categories> 208 | <requires> 209 | <control>pointing</control> 210 | <control>keyboard</control> 211 | <display_length compare="ge">420</display_length> 212 | <internet>always</internet> 213 | </requires> 214 | <recommends> 215 | <control>voice</control> 216 | <display_length compare="ge">760</display_length> 217 | <display_length compare="le">1200</display_length> 218 | </recommends> 219 | <content_rating type="oars-1.1"> 220 | <content_attribute id="social-chat">intense</content_attribute> 221 | <content_attribute id="social-audio">intense</content_attribute> 222 | <content_attribute id="social-contacts">intense</content_attribute> 223 | <content_attribute id="social-info">intense</content_attribute> 224 | </content_rating> 225 | <keywords> 226 | <keyword>Discord</keyword> 227 | <keyword>Sunroof</keyword> 228 | <keyword>Suncord</keyword> 229 | <keyword>Vencord</keyword> 230 | <keyword>Vesktop</keyword> 231 | <keyword>Privacy</keyword> 232 | <keyword>Mod</keyword> 233 | </keywords> 234 | </component> 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<number, NativeImage>(); 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: `<img src="${src.replaceAll('"', '\\"')}">`, 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 extends object>(o: SettingsStore<O>) { 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<MenuItemConstructorOptions | false>; 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<T extends object = any>(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<TSettings>(SETTINGS_FILE, "Sunroof settings"); 38 | 39 | export const VencordSettings = loadSettings<any>(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<TState>(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<string, BrowserWindow>(); 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 <const>{ action: "deny" }; 90 | } 91 | 92 | return <const>{ 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<SpellCheckerResultCallback>(); 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<void>(IpcEvents.RELAUNCH), 25 | getVersion: () => sendSync<void>(IpcEvents.GET_VERSION), 26 | setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count), 27 | supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY) 28 | }, 29 | autostart: { 30 | isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED), 31 | enable: () => invoke<void>(IpcEvents.ENABLE_AUTOSTART), 32 | disable: () => invoke<void>(IpcEvents.DISABLE_AUTOSTART) 33 | }, 34 | fileManager: { 35 | showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path), 36 | getVencordDir: () => sendSync<string | undefined>(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<Settings>(IpcEvents.GET_SETTINGS), 42 | set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path) 43 | }, 44 | spellcheck: { 45 | getAvailableLanguages: () => sendSync<string[]>(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<void>(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, word), 53 | addToDictionary: (word: string) => invoke<void>(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word) 54 | }, 55 | win: { 56 | focus: () => invoke<void>(IpcEvents.FOCUS), 57 | close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key), 58 | minimize: () => invoke<void>(IpcEvents.MINIMIZE), 59 | maximize: () => invoke<void>(IpcEvents.MAXIMIZE) 60 | }, 61 | capturer: { 62 | getLargeThumbnail: (id: string) => invoke<string>(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<void>(IpcEvents.VIRT_MIC_START, include), 71 | startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), 72 | stop: () => invoke<void>(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<void>(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<T = any>(event: IpcEvents, ...args: any[]) { 11 | return ipcRenderer.invoke(event, ...args) as Promise<T>; 12 | } 13 | 14 | export function sendSync<T = any>(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 | <Switch 16 | value={autoStartEnabled} 17 | onChange={async v => { 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 | </Switch> 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 | <Forms.FormText> 15 | The animation on the splash window is loaded from{" "} 16 | {settings.splashAnimationPath ? ( 17 | <a 18 | href="about:blank" 19 | onClick={e => { 20 | e.preventDefault(); 21 | VesktopNative.fileManager.showItemInFolder(settings.splashAnimationPath!); 22 | }} 23 | > 24 | {settings.splashAnimationPath} 25 | </a> 26 | ) : ( 27 | "the default location" 28 | )} 29 | </Forms.FormText> 30 | <div className="vcd-location-btns" style={{ marginBottom: 20 }}> 31 | <Button 32 | size={Button.Sizes.SMALL} 33 | onClick={async () => { 34 | const choice = await VesktopNative.fileManager.selectImagePath(); 35 | if (choice === "cancelled") return; 36 | settings.splashAnimationPath = choice; 37 | }} 38 | > 39 | Change 40 | </Button> 41 | <Button 42 | size={Button.Sizes.SMALL} 43 | color={Button.Colors.RED} 44 | onClick={() => (settings.splashAnimationPath = void 0)} 45 | > 46 | Reset 47 | </Button> 48 | </div> 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 | <Select 14 | placeholder="Stable" 15 | options={[ 16 | { label: "Stable", value: "stable", default: true }, 17 | { label: "Canary", value: "canary" }, 18 | { label: "PTB", value: "ptb" } 19 | ]} 20 | closeOnSelect={true} 21 | select={v => (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 | <Switch 15 | value={settings.appBadge ?? true} 16 | onChange={v => { 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 | </Switch> 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<string, Array<BooleanSetting | SettingsComponent>> = { 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 | <Forms.FormSection 131 | title={title} 132 | key={title} 133 | className="vcd-settings-section" 134 | titleClassName="vcd-settings-title" 135 | > 136 | {settings.map(Setting => { 137 | if (typeof Setting === "function") return <Setting settings={Settings} />; 138 | 139 | const { defaultValue, title, description, key, disabled, invisible } = Setting; 140 | if (invisible?.()) return null; 141 | 142 | return ( 143 | <Switch 144 | value={Settings[key as any] ?? defaultValue} 145 | onChange={v => (Settings[key as any] = v)} 146 | note={description} 147 | disabled={disabled?.()} 148 | key={key} 149 | > 150 | {title} 151 | </Switch> 152 | ); 153 | })} 154 | </Forms.FormSection> 155 | )); 156 | 157 | return <>{sections}</>; 158 | } 159 | 160 | export default function SettingsUi() { 161 | return ( 162 | <Forms.FormSection> 163 | <Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2"> 164 | Sunroof Settings 165 | </Text> 166 | 167 | <SettingsSections /> 168 | </Forms.FormSection> 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 | <Forms.FormText> 19 | Suncord files are loaded from{" "} 20 | {vencordDir ? ( 21 | <a 22 | href="about:blank" 23 | onClick={e => { 24 | e.preventDefault(); 25 | VesktopNative.fileManager.showItemInFolder(vencordDir!); 26 | }} 27 | > 28 | {vencordDir} 29 | </a> 30 | ) : ( 31 | "the default location" 32 | )} 33 | </Forms.FormText> 34 | <div className="vcd-location-btns"> 35 | <Button 36 | size={Button.Sizes.SMALL} 37 | onClick={async () => { 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 | </Button> 63 | <Button 64 | size={Button.Sizes.SMALL} 65 | color={Button.Colors.RED} 66 | onClick={async () => { 67 | await VesktopNative.fileManager.selectVencordDir(null); 68 | forceUpdate(); 69 | }} 70 | > 71 | Reset 72 | </Button> 73 | </div> 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 | <Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>Transparency Options</Forms.FormTitle> 18 | <Forms.FormText className={Margins.bottom8}> 19 | Requires a full restart. You will need a theme that supports transparency for this to work. 20 | </Forms.FormText> 21 | 22 | <Select 23 | placeholder="None" 24 | options={[ 25 | { 26 | label: "None", 27 | value: "none", 28 | default: true 29 | }, 30 | { 31 | label: "Mica (incorporates system theme + desktop wallpaper to paint the background)", 32 | value: "mica" 33 | }, 34 | { label: "Tabbed (variant of Mica with stronger background tinting)", value: "tabbed" }, 35 | { 36 | label: "Acrylic (blurs the window behind Sunroof for a translucent background)", 37 | value: "acrylic" 38 | } 39 | ]} 40 | closeOnSelect={true} 41 | select={v => (settings.transparencyOption = v)} 42 | isSelected={v => v === settings.transparencyOption} 43 | serialize={s => s} 44 | /> 45 | 46 | <Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} /> 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<string, unknown>) => 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<Patch, "plugin">[]; 13 | [key: string]: any; 14 | } 15 | 16 | export function addPatch<P extends PatchData>(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 | <Menu.MenuGroup> 65 | {hasCorrections && ( 66 | <> 67 | {corrections.map(c => ( 68 | <Menu.MenuItem 69 | id={"vcd-spellcheck-suggestion-" + c} 70 | label={c} 71 | action={() => VesktopNative.spellcheck.replaceMisspelling(c)} 72 | /> 73 | ))} 74 | <Menu.MenuSeparator /> 75 | <Menu.MenuItem 76 | id="vcd-spellcheck-learn" 77 | label={`Add ${word} to dictionary`} 78 | action={() => VesktopNative.spellcheck.addToDictionary(word)} 79 | /> 80 | </> 81 | )} 82 | 83 | <Menu.MenuItem id="vcd-spellcheck-settings" label="Spellcheck Settings"> 84 | <Menu.MenuCheckboxItem 85 | id="vcd-spellcheck-enabled" 86 | label="Enable Spellcheck" 87 | checked={spellCheckEnabled} 88 | action={() => { 89 | FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); 90 | }} 91 | /> 92 | 93 | <Menu.MenuItem id="vcd-spellcheck-languages" label="Languages" disabled={!spellCheckEnabled}> 94 | {availableLanguages.map(lang => { 95 | const isEnabled = spellCheckLanguages.includes(lang); 96 | return ( 97 | <Menu.MenuCheckboxItem 98 | id={"vcd-spellcheck-lang-" + lang} 99 | label={lang} 100 | checked={isEnabled} 101 | disabled={!isEnabled && spellCheckLanguages.length >= 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 | </Menu.MenuItem> 114 | </Menu.MenuItem> 115 | </Menu.MenuGroup> 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<T, P> = P extends `${infer Pre}.${infer Suf}` 11 | ? Pre extends keyof T 12 | ? ResolvePropDeep<T[Pre], Suf> 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<T extends object> { 23 | private pathListeners = new Map<string, Set<(newData: any) => 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<P extends LiteralUnion<keyof T, string>>( 133 | path: P, 134 | cb: (data: ResolvePropDeep<T, P>) => 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<keyof T, string>, 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<T extends Function>(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<T>(item: T): item is Exclude<T, 0 | "" | false | null | undefined> { 8 | return Boolean(item); 9 | } 10 | 11 | export function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> { 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<T extends Function>(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<void> { 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 | <head> 2 | <link rel="stylesheet" href="./style.css" type="text/css" /> 3 | 4 | <style> 5 | body { 6 | padding: 2em; 7 | } 8 | 9 | h1 { 10 | text-align: center; 11 | } 12 | </style> 13 | </head> 14 | 15 | <body> 16 | <h1 id="title">Sunroof</h1> 17 | <p> 18 | Sunroof is a free/libre cross platform desktop app aiming to give you a snappier Discord experience with Suncord 19 | pre-installed 20 | </p> 21 | 22 | <section> 23 | <h2>Links</h2> 24 | <ul> 25 | <li> 26 | <a href="https://github.com/verticalsync/Suncord" target="_blank">Suncord Website</a> 27 | </li> 28 | <li> 29 | <a href="https://github.com/verticalsync/Sunroof" target="_blank">Sunroof Source Code</a> 30 | </li> 31 | <li> 32 | <a href="https://github.com/verticalsync/Sunroof/issues" target="_blank">Report bugs / Request features</a> 33 | </li> 34 | </ul> 35 | </section> 36 | 37 | <section> 38 | <h2>Acknowledgements</h2> 39 | <p>These awesome libraries empower Sunroof</p> 40 | <ul> 41 | <li> 42 | <a href="https://github.com/electron/electron" target="_blank">Electron</a> 43 | - Build cross-platform desktop apps with JavaScript, HTML, and CSS 44 | </li> 45 | <li> 46 | <a href="https://github.com/electron-userland/electron-builder" target="_blank">Electron Builder</a> 47 | - A complete solution to package and build a ready for distribution Electron app with “auto update” 48 | support out of the box 49 | </li> 50 | <li> 51 | <a href="https://github.com/OpenAsar/arrpc" target="_blank">arrpc</a> 52 | - An open implementation of Discord's Rich Presence server 53 | </li> 54 | <li> 55 | <a href="https://github.com/Soundux/rohrkabel" target="_blank">rohrkabel</a> 56 | - A C++ RAII Pipewire-API Wrapper 57 | </li> 58 | <li> 59 | And many 60 | <a href="https://github.com/verticalsync/Sunroof/blob/main/pnpm-lock.yaml" target="_blank" 61 | >more awesome open source libraries</a 62 | > 63 | </li> 64 | </ul> 65 | </section> 66 | </body> 67 | 68 | <script type="module"> 69 | const data = await Updater.getData(); 70 | if (data.currentVersion) { 71 | const title = document.getElementById("title"); 72 | 73 | title.textContent += ` v${data.currentVersion}`; 74 | } 75 | </script> 76 | -------------------------------------------------------------------------------- /static/views/first-launch.html: -------------------------------------------------------------------------------- 1 | <head> 2 | <link rel="stylesheet" href="./style.css" type="text/css" /> 3 | 4 | <style> 5 | body { 6 | height: 100vh; 7 | 8 | padding: 1.5em; 9 | padding-bottom: 1em; 10 | 11 | border: 1px solid var(--fg-semi-trans); 12 | border-top: none; 13 | display: flex; 14 | flex-direction: column; 15 | box-sizing: border-box; 16 | } 17 | 18 | select { 19 | background: var(--bg); 20 | color: var(--fg); 21 | padding: 0.3em; 22 | margin: -0.3em; 23 | border-radius: 6px; 24 | } 25 | 26 | h1 { 27 | margin: 0.4em 0 0; 28 | } 29 | 30 | p { 31 | margin: 1em 0 2em; 32 | } 33 | 34 | form { 35 | display: grid; 36 | gap: 1em; 37 | margin: 0; 38 | } 39 | 40 | label { 41 | position: relative; 42 | display: flex; 43 | justify-content: space-between; 44 | } 45 | 46 | label:has(input[type="checkbox"]), 47 | select { 48 | cursor: pointer; 49 | } 50 | 51 | label:not(:last-child)::after { 52 | content: ""; 53 | position: absolute; 54 | bottom: -10px; 55 | width: 100%; 56 | height: 1px; 57 | background-color: var(--fg-secondary); 58 | opacity: 0.5; 59 | } 60 | 61 | label div { 62 | display: grid; 63 | gap: 0.2em; 64 | } 65 | 66 | label h2 { 67 | margin: 0; 68 | font-weight: normal; 69 | font-size: 1.1rem; 70 | line-height: 1rem; 71 | } 72 | 73 | label span { 74 | font-size: 0.9rem; 75 | font-weight: 400; 76 | color: var(--fg-secondary); 77 | } 78 | 79 | #buttons { 80 | display: flex; 81 | justify-content: end; 82 | gap: 0.5em; 83 | margin-top: auto; 84 | } 85 | 86 | button { 87 | padding: 0.6em; 88 | background: red; 89 | color: white; 90 | border-radius: 6px; 91 | border: none; 92 | cursor: pointer; 93 | transition: 200ms filter; 94 | } 95 | 96 | button:hover { 97 | filter: brightness(0.8); 98 | } 99 | 100 | #submit { 101 | background: green; 102 | } 103 | </style> 104 | </head> 105 | 106 | <body> 107 | <h1>Welcome to Sunroof</h1> 108 | <p>Let's customise your experience!</p> 109 | 110 | <form> 111 | <label> 112 | <h2>Discord Branch</h2> 113 | <select name="discordBranch"> 114 | <option value="stable">stable</option> 115 | <option value="canary">canary</option> 116 | <option value="ptb">ptb</option> 117 | </select> 118 | </label> 119 | 120 | <label> 121 | <div> 122 | <h2>Start with System</h2> 123 | <span>Automatically open Sunroof when your computer starts</span> 124 | </div> 125 | <input type="checkbox" name="autoStart" /> 126 | </label> 127 | 128 | <label> 129 | <div> 130 | <h2>Rich Presence</h2> 131 | <span 132 | >Enable Rich presence (game activity) via 133 | <a href="https://github.com/OpenAsar/arrpc" target="_blank">arRPC</a></span 134 | > 135 | </div> 136 | <input type="checkbox" name="richPresence" checked /> 137 | </label> 138 | 139 | <label> 140 | <div> 141 | <h2>Import Settings</h2> 142 | <span>Import Settings from existing Suncord install (if found)</span> 143 | </div> 144 | <input type="checkbox" name="importSettings" checked /> 145 | </label> 146 | 147 | <label> 148 | <div> 149 | <h2>Minimise to Tray</h2> 150 | <span>Minimise to Tray when closing</span> 151 | </div> 152 | <input type="checkbox" name="minimizeToTray" checked /> 153 | </label> 154 | </form> 155 | <div id="buttons"> 156 | <button id="cancel">Quit</button> 157 | <button id="submit">Submit</button> 158 | </div> 159 | </body> 160 | 161 | <script> 162 | cancel.onclick = () => console.info("cancel"); 163 | submit.onclick = e => { 164 | const form = document.querySelector("form"); 165 | const formData = new FormData(form); 166 | const data = Object.fromEntries(formData.entries()); 167 | console.info("form:" + JSON.stringify(data)); 168 | e.preventDefault(); 169 | }; 170 | </script> 171 | -------------------------------------------------------------------------------- /static/views/splash.html: -------------------------------------------------------------------------------- 1 | <head> 2 | <link rel="stylesheet" href="./style.css" type="text/css" /> 3 | 4 | <style> 5 | body { 6 | user-select: none; 7 | -webkit-app-region: drag; 8 | } 9 | 10 | .wrapper { 11 | box-sizing: border-box; 12 | height: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | border-radius: 8px; 18 | border: 1px solid var(--fg-semi-trans); 19 | } 20 | 21 | p { 22 | text-align: center; 23 | } 24 | 25 | img { 26 | width: 128px; 27 | } 28 | </style> 29 | </head> 30 | 31 | <body> 32 | <div class="wrapper"> 33 | <!-- the data url is here to ensure there isn't an empty frame before the image is loaded --> 34 | <img id="animation" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" 35 | draggable="false" alt="animation" role="presentation" /> 36 | <p>Loading Sunroof...</p> 37 | </div> 38 | </body> 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 | <head> 2 | <link rel="stylesheet" href="./style.css" type="text/css" /> 3 | 4 | <style> 5 | .wrapper { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | box-sizing: border-box; 10 | min-height: 100%; 11 | padding: 1em; 12 | } 13 | 14 | h1 { 15 | text-align: center; 16 | } 17 | 18 | .buttons { 19 | display: grid; 20 | grid-template-columns: 1fr 1fr; 21 | gap: 0.5em; 22 | margin-top: 0.25em; 23 | } 24 | 25 | button { 26 | cursor: pointer; 27 | padding: 0.5em; 28 | color: var(--fg); 29 | border: none; 30 | border-radius: 3px; 31 | font-weight: bold; 32 | transition: filter 0.2 ease-in-out; 33 | } 34 | 35 | button:hover, 36 | button:active { 37 | filter: brightness(0.9); 38 | } 39 | 40 | .green { 41 | background-color: #248046; 42 | } 43 | 44 | .red { 45 | background-color: #ed4245; 46 | } 47 | </style> 48 | </head> 49 | 50 | <body> 51 | <div class="wrapper"> 52 | <section> 53 | <h1>Update Available</h1> 54 | <p>There's a new update for Sunroof! Update now to get new fixes and features!</p> 55 | <p> 56 | Current: <span id="current"></span> 57 | <br /> 58 | Latest: <span id="latest"></span> 59 | </p> 60 | 61 | <h2>Changelog</h2> 62 | <p id="changelog">Loading...</p> 63 | </section> 64 | 65 | <section> 66 | <label id="disable-remind"> 67 | <input type="checkbox" /> 68 | <span>Do not remind again for </span> 69 | </label> 70 | 71 | <div class="buttons"> 72 | <button name="download" class="green">Download Update</button> 73 | <button name="close" class="red">Close</button> 74 | </div> 75 | </section> 76 | </div> 77 | </body> 78 | 79 | <script type="module"> 80 | const data = await Updater.getData(); 81 | document.getElementById("current").textContent = data.currentVersion; 82 | document.getElementById("latest").textContent = data.latestVersion; 83 | 84 | document.querySelector("#disable-remind > span").textContent += data.latestVersion; 85 | 86 | function checkDisableRemind() { 87 | const checkbox = document.querySelector("#disable-remind > input"); 88 | if (checkbox.checked) { 89 | Updater.ignore(); 90 | } 91 | } 92 | 93 | const onClicks = { 94 | download() { 95 | checkDisableRemind(); 96 | Updater.download(); 97 | }, 98 | close() { 99 | checkDisableRemind(); 100 | Updater.close(); 101 | } 102 | }; 103 | 104 | for (const name in onClicks) { 105 | document.querySelectorAll(`button[name="${name}"]`).forEach(button => { 106 | button.addEventListener("click", onClicks[name]); 107 | }); 108 | } 109 | </script> 110 | 111 | <script type="module"> 112 | import { micromark } from "https://esm.sh/micromark@3?bundle"; 113 | import { gfm, gfmHtml } from "https://esm.sh/micromark-extension-gfm@2?bundle"; 114 | 115 | const changelog = (await Updater.getData()).release.body; 116 | if (changelog) 117 | document.getElementById("changelog").innerHTML = micromark(changelog, { 118 | extensions: [gfm()], 119 | htmlExtensions: [gfmHtml()] 120 | }) 121 | .replace(/h1>/g, "h3>") 122 | .replace(/<a /g, '<a target="_blank" '); 123 | </script> 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 | --------------------------------------------------------------------------------