├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── codeql.yml │ ├── main.yml │ └── testBuild.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── assets ├── Wordmark.svg ├── adblocker.js ├── badges │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── css │ ├── discord.css │ ├── screenshare.css │ ├── settings.css │ └── titlebar.css ├── fonts │ └── InterVariable.woff2 ├── gf_icon.ico ├── gf_icon.png ├── gf_logo.svg ├── gf_symbolic_black.png ├── gf_symbolic_white.png ├── html │ ├── multiselect-dropdown.js │ ├── picker.html │ ├── settings.html │ └── settings.js └── lang │ ├── cs.json │ ├── de.json │ ├── en-US.json │ ├── es.json │ ├── fil.json │ ├── fr.json │ ├── nb_NO.json │ ├── nds.json │ ├── nl.json │ ├── pt.json │ ├── sl.json │ ├── ta.json │ ├── tr.json │ ├── uk.json │ └── zh_Hant.json ├── assetsDev ├── genTestLanguage.js ├── gf_install_animation.gif ├── gf_logo_full.png ├── gf_logo_orig.png ├── gf_symbolic.svg ├── io.github.milkshiift.GoofCord.metainfo.xml ├── languageCodes.json ├── screenshot1.png ├── screenshot1_rounded.png ├── wikiEncryption1.png ├── wikiEncryption2.png ├── wikiEncryption3.png └── wikiSceenShare.png ├── biome.json ├── build ├── build.ts ├── cursedJson.ts ├── genIpcHandlers.ts ├── genSettingsLangFile.ts ├── genSettingsTypes.ts ├── icon.icns ├── icon.ico ├── icon.png └── installer.nsh ├── bun.lock ├── electron-builder.ts ├── package.json ├── src ├── config.ts ├── configTypes.d.ts ├── ipcGen.ts ├── loader.ts ├── main.ts ├── menu.ts ├── modules │ ├── agent.ts │ ├── arrpc.ts │ ├── assetLoader.ts │ ├── cacheManager.ts │ ├── dynamicIcon.ts │ ├── firewall.ts │ ├── localization.ts │ ├── logger.ts │ ├── messageEncryption.ts │ ├── mods.ts │ ├── updateCheck.ts │ ├── venbind.ts │ ├── venmic.ts │ └── windowStateManager.ts ├── settingsSchema.ts ├── tray.ts ├── utils.ts └── windows │ ├── main │ ├── bridge.ts │ ├── defaultAssets.ts │ ├── keybinds.ts │ ├── main.ts │ ├── preload.mts │ ├── screenshare.ts │ └── titlebar.ts │ ├── preloadUtils.ts │ ├── screenshare │ ├── main.ts │ └── preload.mts │ └── settings │ ├── cloud │ ├── cloud.ts │ ├── encryption.ts │ └── token.ts │ ├── main.ts │ ├── preload.mts │ └── settingsRenderer.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | - [ ] I have searched and found no existing issues on the same topic 10 | - [ ] I have searched the [wiki](https://github.com/Milkshiift/GoofCord/wiki/FAQ) and found no mention of my problem 11 | 12 | ### Description 13 | 14 | ### Steps to Reproduce 15 | 16 | ### Expected Behavior 17 | 18 | ### Actual Behavior 19 | 20 | ### Screenshots 21 | 22 | ### Environment 23 | - Operating System (name and version): 24 | - GoofCord Version: 25 | - Way of installing: 26 | 27 | ### Additional Information 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Discord 3 | url: https://discord.gg/CZc4bpnjmm 4 | about: Ask anything non-bug/feature related here (such as "How does XYZ work?" or "Why does ZYX work the way it does?") 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I have searched and found no existing issues on the same topic 11 | 12 | ### Description 13 | 14 | ### Use Case 15 | 16 | ### Describe alternatives you've considered 17 | 18 | ### Additional context 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '22 3 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release app 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | include: 14 | - name: linux 15 | runner: ubuntu-latest 16 | targets: "tar.xz AppImage deb rpm" 17 | arch: "--x64 --arm64 --armv7l" 18 | - name: win 19 | runner: windows-latest 20 | targets: "zip nsis" 21 | arch: "--x64 --ia32 --arm64" 22 | - name: mac 23 | runner: macos-latest 24 | targets: "dmg" 25 | arch: "--x64 --arm64" 26 | 27 | name: ${{ matrix.name }} 28 | runs-on: ${{ matrix.runner }} 29 | steps: 30 | - name: Github checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Install Python setuptools 34 | if: matrix.name == 'mac' 35 | run: brew install python-setuptools 36 | 37 | - uses: oven-sh/setup-bun@v2 38 | with: 39 | bun-version: latest 40 | 41 | - name: Use Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: "23.x" 45 | 46 | - name: Install Node dependencies 47 | run: bun install --frozen-lockfile 48 | 49 | - name: Install electron-builder globally 50 | run: bun add electron-builder -g 51 | 52 | - name: Build GoofCord 53 | run: bun run build 54 | 55 | - name: Package GoofCord 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: electron-builder ${{ matrix.arch }} --${{ matrix.name }} ${{ matrix.targets }} --publish=always -------------------------------------------------------------------------------- /.github/workflows/testBuild.yml: -------------------------------------------------------------------------------- 1 | name: Test build 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | include: 14 | - name: linux 15 | runner: ubuntu-latest 16 | targets: "tar.xz AppImage deb rpm" 17 | arch: "--x64 --arm64 --armv7l" 18 | - name: win 19 | runner: windows-latest 20 | targets: "zip" 21 | arch: "--x64 --ia32 --arm64" 22 | - name: mac 23 | runner: macos-latest 24 | targets: "dmg" 25 | arch: "--x64 --arm64" 26 | 27 | name: ${{ matrix.name }} 28 | runs-on: ${{ matrix.runner }} 29 | steps: 30 | - name: Github checkout 31 | uses: actions/checkout@v4 32 | 33 | - uses: pnpm/action-setup@v4 34 | with: 35 | version: latest 36 | 37 | - name: Use Node.js 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: "22.x" 41 | cache: "pnpm" 42 | 43 | - name: Install Node dependencies 44 | run: pnpm install --frozen-lockfile 45 | 46 | - name: Install electron-builder globally 47 | run: pnpm add electron-builder -g 48 | 49 | - name: Build GoofCord 50 | run: pnpm run build 51 | 52 | - name: Package GoofCord 53 | run: electron-builder ${{ matrix.arch }} --${{ matrix.name }} ${{ matrix.targets }} --publish=never 54 | 55 | - name: Upload Artifacts 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: ${{ matrix.name }}-artifacts 59 | path: | 60 | dist/**/*.tar.xz 61 | dist/**/*.AppImage 62 | dist/**/*.deb 63 | dist/**/*.rpm 64 | dist/**/*.zip 65 | dist/**/*.dmg 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out/ 3 | dist 4 | ts-out/ 5 | ts-out 6 | .idea 7 | .vscode/ 8 | assets/lang/test-TEST.json 9 | assets/venmic* 10 | assets/venbind* -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | public-hoist-pattern=* 3 | shamefully-hoist=true 4 | engine-strict=false 5 | virtual-store-dir-max-length=70 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | GoofCord logo 3 |

Take control of your Discord experience with GoofCord – the highly configurable and privacy-focused Discord client.
Based on Legcord

4 | 5 | 6 | 7 | 8 | Translation status 9 |
10 | Screenshot of GoofCord 11 |
12 | 13 | ## Features :sparkles: 14 | - **:lock: With Privacy in mind**: GoofCord blocks all tracking and uses multiple techniques like message encryption to improve your privacy and security. [Learn more](https://github.com/Milkshiift/GoofCord/wiki/Privacy-FAQ) 15 | - **:chart_with_upwards_trend: Fast and Performant**: Glide through your chats with GoofCord's superior speed and responsiveness compared to the official client. 16 | - **:bookmark: Standalone**: GoofCord is a standalone application, not reliant on the original Discord client in any way. 17 | - **:electric_plug: Plugins & Themes support**: Easily use client mods like [Vencord](https://github.com/Vendicated/Vencord), [Equicord](https://github.com/Equicord/Equicord) or [Shelter](https://github.com/uwu/shelter) for plugins and themes. 18 | - **⌨️ Global Keybinds**: Set up keybinds and use them across the system 19 | - **🐧 Linux support**: Seamless screensharing with audio and native Wayland support on Linux. See FAQ for details. 20 | 21 | ## Installation 22 | ### Windows 🪟 23 | 24 | * Install with prebuilt binaries from the [releases](https://github.com/Milkshiift/GoofCord/releases/latest) page. 25 | Choose `GoofCord-Setup-.exe` for an installer, or 26 | `GoofCord--win-.zip` to manually unpack into a directory of your choice. 27 | * Install using **winget**: `winget install GoofCord` 28 | 29 | ### Linux 🐧 30 | 31 | * Install with prebuilt binaries from the [releases](https://github.com/Milkshiift/GoofCord/releases/latest) page. 32 | * Install from [Flathub](https://flathub.org/apps/io.github.milkshiift.GoofCord) 33 | * Install from [AUR](https://aur.archlinux.org/packages/goofcord-bin) if you run an **Arch**-based OS. Here's an example using yay: 34 | `yay -S goofcord-bin` 35 | Keep in mind that the AUR package is not maintained by the developers of GoofCord. 36 | * Install in **NixOS** from [nixpkgs](https://search.nixos.org/packages?channel=unstable&query=goofcord). 37 | 38 | ### macOS 🍏 39 | Note: As I don't have a macOS device, macOS support is limited. 40 | * Install with prebuilt binaries from the [releases](https://github.com/Milkshiift/GoofCord/releases/latest) page. 41 | Choose the file ending with `mac-arm64.dmg` if your computer uses an Apple Silicon processor. [Mac computers with Apple Silicon](https://support.apple.com/en-us/HT211814) 42 | Otherwise, choose the file that ends with `mac-x64.dmg` 43 | * If you get an error like "GoofCord is damaged and can't be opened" see [this issue](https://github.com/Milkshiift/GoofCord/issues/7) 44 | 45 | To explore plugins and themes, head over to the Vencord category in the Discord settings. 46 | 47 | And if you want to compile it yourself, here's how: 48 | 1. Install [Node.js](https://nodejs.dev) *and* [Bun](https://bun.sh) for package management and bundling. 49 | 2. Grab the source code from the latest release. Getting it from the main branch is not recommended for a stable experience. 50 | 3. Open a command line in the directory of the source code 51 | 4. Install the dependencies with `bun install` 52 | 5. Package GoofCord with either `bun run packageWindows`, `bun run packageLinux` or `bun run packageMac` 53 | 6. Find your freshly compiled app in the `dist` folder. 54 | 55 | ## Short FAQ 56 | ### Need Support? Join Our Discord! 57 | [![](https://dcbadge.vercel.app/api/server/CZc4bpnjmm)](https://discord.gg/CZc4bpnjmm) 58 | 59 | ### Where is the long FAQ? 60 | - [On the Wiki](https://github.com/Milkshiift/GoofCord/wiki/FAQ) 61 | 62 | ### How do I develop GoofCord? 63 | - See the development [guide](https://github.com/Milkshiift/GoofCord/wiki/How-to-develop-GoofCord) 64 | 65 | ### Can I get banned from using GoofCord? 🤔 66 | - While using GoofCord goes against [Discord ToS](https://discord.com/terms#software-in-discord%E2%80%99s-services), no one has ever been banned from using it or any client mods. 67 | 68 | ### How can I access the settings? ⚙️ 69 | - Multiple ways: 70 | - Right-click on the tray icon and click `Open Settings` 71 | - Click the "Settings" button in the "GoofCord" category in the Discord settings 72 | - Press `Ctrl+Shift+'` shortcut. 73 | 74 | ### How do I run GoofCord natively on Wayland? 75 | - GoofCord should run natively out of the box, but if it doesn't, run with these arguments: 76 | `--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto` 77 | If GoofCord shows a black screen, also include this argument: `--disable-gpu-sandbox` 78 | 79 | ### Seeking the Source Code? 🕵️‍♂️ 80 | - You can find our source code on [GitHub](https://github.com/Milkshiift/GoofCord/). 81 | 82 | ### Check out our [wiki](https://github.com/Milkshiift/GoofCord/wiki) if you've got questions left 83 | 84 | ## Donations 85 | If you like GoofCord, you can support me with crypto: 86 | - **XMR (Monero)**: `44FyEbizgCbCaghrtCp2BGQ7WZcNRkwAMNEf9fUzgu6A3wmQq8yqrHiAMu2jT784k6NcSByJUApk8jMREMmUJQeu9g6Dxbq` 87 | - **USDT (Arbitrum/BEP20)**: `0xcacf4a4089c5a68657f2b39d8935a1ec01f999b8` 88 | - **BTC**: `3PRgLrYWzojWHur8WKKNRwpXwzG6J5Zf3K` 89 | -------------------------------------------------------------------------------- /assets/Wordmark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/badges/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/1.png -------------------------------------------------------------------------------- /assets/badges/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/10.png -------------------------------------------------------------------------------- /assets/badges/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/2.png -------------------------------------------------------------------------------- /assets/badges/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/3.png -------------------------------------------------------------------------------- /assets/badges/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/4.png -------------------------------------------------------------------------------- /assets/badges/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/5.png -------------------------------------------------------------------------------- /assets/badges/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/6.png -------------------------------------------------------------------------------- /assets/badges/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/7.png -------------------------------------------------------------------------------- /assets/badges/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/8.png -------------------------------------------------------------------------------- /assets/badges/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/badges/9.png -------------------------------------------------------------------------------- /assets/css/discord.css: -------------------------------------------------------------------------------- 1 | /* Hide download discord button */ 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 | -------------------------------------------------------------------------------- /assets/css/titlebar.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --wordmark-svg: url("data:image/svg+xml,%3Csvg version='1.2' baseProfile='tiny' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 14' overflow='visible' xml:space='preserve'%3E%3Cpath fill='%2372767D' d='M7.3 4.3c-.7-1.8-3.8-1.4-3.8.4v4.7c0 2.1 4 2.1 4 0v-.5H5.3V6h5.5v3.4c0 3.1-2.6 4.7-5.3 4.7-2.7 0-5.4-1.6-5.4-4.7V4.7C.1 1.6 2.8.1 5.5.1 7.3.1 9.3.8 10.2 3L7.3 4.3zM12.1 4.8c0-3.1 2.8-4.7 5.6-4.7s5.6 1.6 5.6 4.7v4.7c0 3.1-2.8 4.7-5.6 4.7s-5.6-1.6-5.6-4.7V4.8zm3.5 4.6c0 1 1.1 1.5 2.2 1.5s2.2-.5 2.2-1.5V4.8c0-1.1-1.1-1.5-2.2-1.5s-2.1.5-2.1 1.5v4.6zM24.9 4.8c0-3.1 2.8-4.7 5.6-4.7s5.6 1.6 5.6 4.7v4.7c0 3.1-2.8 4.7-5.6 4.7s-5.6-1.6-5.6-4.7V4.8zm3.4 4.6c0 1 1.1 1.5 2.2 1.5s2.2-.5 2.2-1.5V4.8c0-1.1-1.1-1.5-2.2-1.5s-2.1.5-2.1 1.5v4.6zM41.3 6h5.2v3.2h-5.2v4.7h-3.5V.3h9.5v3.3h-6V6zM58.8 11.1c-1.1 2.2-3.2 3.1-4.9 3.1-2.7 0-5.4-1.6-5.4-4.7V4.8c0-3.1 2.7-4.7 5.4-4.7 1.7 0 3.7.7 4.8 3.1L56 4.4c-.9-1.7-4-1.3-4 .4v4.7c0 1.6 3.1 2.1 3.8.2l3 1.4zM59.9 4.8c0-3.1 2.8-4.7 5.6-4.7s5.6 1.6 5.6 4.7v4.7c0 3.1-2.8 4.7-5.6 4.7s-5.6-1.6-5.6-4.7V4.8zm3.5 4.6c0 1 1.1 1.5 2.2 1.5s2.2-.5 2.2-1.5V4.8c0-1.1-1.1-1.5-2.2-1.5s-2.2.5-2.2 1.5v4.6zM77 9.6h-.6v4.3h-3.5V.3h5.4c2.8 0 5 1.3 5 4.5 0 2.5-1 3.9-2.7 4.5l3.7 4.7H80l-3-4.4zm1.4-3c2.2 0 2.2-3.1 0-3.1h-2v3.1h2z'/%3E%3Cpath fill-rule='evenodd' fill='%2372767D' d='M85.6.1h3.1c2.5 0 3.2 0 3.9.2.5.2 1.1.4 1.6.7.4.3.9.8 1.1 1.2.3.3.5.8.6 1 .1.3.2 1.7.2 3.2 0 1.9 0 3.1-.1 3.7-.1.5-.3 1.1-.4 1.3-.1.3-.5.7-.8 1-.2.3-.8.7-1.2.9-.4.2-1.1.4-1.6.5-.4 0-2.1.1-6.4.1v-3.4h2.6c1.5 0 2.9 0 3.1-.1.3-.1.7-.2.8-.4.4-.3.4-.4.4-2.9 0-2 0-2.6-.2-2.9-.1-.2-.3-.5-.5-.6-.3-.1-.8-.1-2.6-.1V6c0 1.5-.1 2.7-.1 2.7-.1.1-.9-.7-3.5-3.3V.1z'/%3E%3C/svg%3E"); 3 | } 4 | 5 | [data-windows] { 6 | transition: background-color 0.25s ease-in; 7 | will-change: background-color; 8 | } 9 | 10 | /* GoofCord logo */ 11 | [data-windows][__goofcord-custom-titlebar="true"] > [class^="leading"]::after { 12 | content: ""; 13 | width: 80px; 14 | mask-image: var(--wordmark-svg); 15 | mask-repeat: no-repeat; 16 | background-color: var(--text-muted); 17 | height: 9px; 18 | } 19 | 20 | /* Move top right buttons to the left to make space for 21 | max/min/close buttons */ 22 | [data-windows][__goofcord-custom-titlebar="true"] > [class^="trailing"] { 23 | padding-right: 100px !important; 24 | } 25 | 26 | #titlebar-text { 27 | will-change: opacity; 28 | position: absolute; 29 | left: 50%; 30 | transform: translateX(-50%); 31 | color: white; 32 | background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(20, 20, 20, 1) 50%, rgba(0, 0, 0, 0) 100%); 33 | padding: 6px 120px; 34 | margin: 0; 35 | max-width: 1000px; 36 | height: 25px; 37 | vertical-align: middle; 38 | line-height: 150%; 39 | opacity: 0; 40 | z-index: 999; 41 | font-weight: bold; 42 | letter-spacing: 0.02em; 43 | } 44 | 45 | #dragbar { 46 | position: absolute; 47 | width: 100%; 48 | height: 37px; 49 | z-index: 998; 50 | transition: background-color 0.25s ease-in; 51 | } 52 | 53 | #window-controls-container { 54 | width: 100px; 55 | height: 32px; 56 | position: absolute; 57 | top: 0; 58 | right: 11px; 59 | z-index: 999; 60 | -webkit-app-region: no-drag; 61 | } 62 | 63 | #window-controls-container > div { 64 | float: left; 65 | width: 33.3%; 66 | height: 100%; 67 | mask-size: 40%; 68 | mask-repeat: no-repeat; 69 | mask-position: center; 70 | background-color: var(--interactive-normal); 71 | cursor: pointer; 72 | } 73 | 74 | #window-controls-container > div:hover { 75 | background-color: var(--interactive-hover); 76 | } 77 | 78 | #minimize { 79 | mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14 8v1H3V8h11z'/%3E%3C/svg%3E"); 80 | } 81 | 82 | #maximize { 83 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 3v10h10V3H3zm9 9H4V4h8v8z'/%3E%3C/svg%3E"); 84 | } 85 | 86 | #maximized { 87 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 5v9h9V5H3zm8 8H4V6h7v7z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 5h1V4h7v7h-1v1h2V3H5v2z'/%3E%3C/svg%3E"); 88 | } 89 | 90 | #quit { 91 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.116 8l-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z'/%3E%3C/svg%3E"); 92 | } 93 | 94 | #quit:hover { 95 | background-color: var(--red-360) !important; 96 | } 97 | -------------------------------------------------------------------------------- /assets/fonts/InterVariable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/fonts/InterVariable.woff2 -------------------------------------------------------------------------------- /assets/gf_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/gf_icon.ico -------------------------------------------------------------------------------- /assets/gf_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/gf_icon.png -------------------------------------------------------------------------------- /assets/gf_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/gf_symbolic_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/gf_symbolic_black.png -------------------------------------------------------------------------------- /assets/gf_symbolic_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assets/gf_symbolic_white.png -------------------------------------------------------------------------------- /assets/html/multiselect-dropdown.js: -------------------------------------------------------------------------------- 1 | function MultiselectDropdown(options) { 2 | const config = { 3 | placeholder: 'Select...', 4 | ...options 5 | }; 6 | 7 | function createElement(tag, attrs = {}) { 8 | const element = document.createElement(tag); 9 | 10 | Object.entries(attrs).forEach(([key, value]) => { 11 | if (key === 'class') { 12 | if (Array.isArray(value)) { 13 | value.filter(cls => cls !== '').forEach(cls => element.classList.add(cls)); 14 | } else if (value !== '') { 15 | element.classList.add(value); 16 | } 17 | } else if (key === 'style') { 18 | Object.entries(value).forEach(([styleKey, styleValue]) => { 19 | element.style[styleKey] = styleValue; 20 | }); 21 | } else if (key === 'text') { 22 | element.textContent = value === '' ? '\u00A0' : value; 23 | } else if (key === 'html') { 24 | element.innerHTML = value; 25 | } else { 26 | element[key] = value; 27 | } 28 | }); 29 | 30 | return element; 31 | } 32 | 33 | document.querySelectorAll("select[multiple]").forEach(selectElement => { 34 | selectElement.style.display = 'none'; 35 | 36 | const dropdownContainer = createElement('div', { 37 | class: 'multiselect-dropdown', 38 | role: 'listbox', 39 | 'aria-label': selectElement.getAttribute('aria-label') || 'Multiselect dropdown', 40 | tabIndex: 0 41 | }); 42 | 43 | selectElement.parentNode.insertBefore(dropdownContainer, selectElement.nextSibling); 44 | 45 | const listWrapper = createElement('div', { 46 | class: ['multiselect-dropdown-list-wrapper', 'dropdown-hidden'] 47 | }); 48 | 49 | const optionsList = createElement('div', { 50 | class: 'multiselect-dropdown-list', 51 | role: 'group' 52 | }); 53 | 54 | dropdownContainer.appendChild(listWrapper); 55 | listWrapper.appendChild(optionsList); 56 | 57 | selectElement.loadOptions = () => { 58 | optionsList.innerHTML = ''; 59 | 60 | Array.from(selectElement.options).forEach(option => { 61 | const optionItem = createElement('div', { 62 | class: option.selected ? 'checked' : '', 63 | optEl: option, 64 | role: 'option', 65 | 'aria-selected': option.selected 66 | }); 67 | 68 | const optionLabel = createElement('label', { 69 | text: option.text 70 | }); 71 | 72 | optionItem.appendChild(optionLabel); 73 | 74 | optionItem.addEventListener('click', (e) => { 75 | e.stopPropagation(); 76 | option.selected = !option.selected; 77 | optionItem.setAttribute('aria-selected', option.selected); 78 | optionItem.classList.toggle('checked', option.selected); 79 | selectElement.dispatchEvent(new Event('change')); 80 | refreshDropdown(); 81 | }); 82 | 83 | option.listitemEl = optionItem; 84 | optionsList.appendChild(optionItem); 85 | }); 86 | }; 87 | 88 | const refreshDropdown = () => { 89 | dropdownContainer.querySelectorAll('span.optext, span.placeholder').forEach(el => 90 | dropdownContainer.removeChild(el) 91 | ); 92 | 93 | const selectedOptions = Array.from(selectElement.selectedOptions); 94 | 95 | if (selectedOptions.length === 0) { 96 | dropdownContainer.appendChild(createElement('span', { 97 | class: 'placeholder', 98 | text: config.placeholder 99 | })); 100 | } else { 101 | selectedOptions.forEach(option => { 102 | const tag = createElement('span', { 103 | class: 'optext', 104 | text: option.text, 105 | srcOption: option 106 | }); 107 | 108 | dropdownContainer.appendChild(tag); 109 | }); 110 | } 111 | }; 112 | 113 | selectElement.loadOptions(); 114 | refreshDropdown(); 115 | 116 | dropdownContainer.addEventListener('click', () => { 117 | listWrapper.classList.toggle('dropdown-hidden'); 118 | dropdownContainer.classList.toggle('open'); 119 | }); 120 | 121 | document.addEventListener('click', (event) => { 122 | if (!dropdownContainer.contains(event.target)) { 123 | listWrapper.classList.add('dropdown-hidden'); 124 | dropdownContainer.classList.remove('open'); 125 | refreshDropdown(); 126 | } 127 | }); 128 | 129 | dropdownContainer.addEventListener('keydown', (e) => { 130 | if (e.key === 'Enter' || e.key === ' ') { 131 | e.preventDefault(); 132 | listWrapper.classList.toggle('dropdown-hidden'); 133 | } else if (e.key === 'Escape') { 134 | listWrapper.classList.add('dropdown-hidden'); 135 | } 136 | }); 137 | 138 | selectElement.addEventListener('change', refreshDropdown); 139 | }); 140 | } 141 | 142 | window.initMultiselect = () => { 143 | MultiselectDropdown(window.MultiselectDropdownOptions || {}); 144 | }; -------------------------------------------------------------------------------- /assets/html/picker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GoofCord Screenshare 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/html/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/lang/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "inviteMessage": "Chcete se přidat k", 3 | "screenshare-optimization-motion": "Pohyb", 4 | "screenshare-screenshare": "Sdílení Obrazovky", 5 | "menu-edit-copy": "Kopírovat", 6 | "menu-development-devtools": "Otevřít Vývojářské Nástroje", 7 | "no": "Ne", 8 | "goofcord-about": "O GoofCordu", 9 | "goofcord-quit": "Ukončit", 10 | "goofcord-open": "Otevřít GoofCord", 11 | "goofcord-fullScreen": "Celá Obrazovka", 12 | "goofcord-reload": "Načíst znovu", 13 | "menu-edit-undo": "Vrátit zpět", 14 | "menu-edit-redo": "Znovu provést", 15 | "menu-zoom": "Lupa", 16 | "menu-zoom-in": "Přiblížit", 17 | "menu-zoom-out": "Oddálit", 18 | "menu-development": "Vývoj", 19 | "screenshare-optimization-detail": "Detail", 20 | "screenshare-resolution": "Rozlišení", 21 | "screenshare-framerate": "Snímková Frekvence", 22 | "settingsWindow-title": "Nastavení GoofCordu | Verze: ", 23 | "category-other": "Ostatní nastavení", 24 | "category-client": "Nastavení klientských modifikací", 25 | "settings-encryption-unavailable": "GoofCord nebyl schopen nalézt správce hesel. Zašifrovaná nastavení budou uložena jako prostý text. Pokud jste na Linuxu, nainstalujte balíček \"gnome-libsecret\" anebo \"kwallet\".", 26 | "opt-minimizeToTray-desc": "GoofCord zůstane otevřený i po uzavření všech jeho oken.", 27 | "opt-startMinimized": "Spustit minimalizovaně", 28 | "opt-startMinimized-desc": "GoofCord se spustí v pozadí a zůstane mimo vašich očí.", 29 | "opt-dynamicIcon": "Dynamická ikona", 30 | "opt-customIconPath": "Vlastní Ikona", 31 | "opt-modNames": "Klientské modifikace", 32 | "opt-discordUrl": "Odkaz na Discord", 33 | "opt-discordUrl-desc": "Odkaz, který GoofCord načte během spuštění. Přidejte \"canary.\" nebo \"ptb.\" před \"discord.com\" pro příslušné případy.", 34 | "opt-firewall": "Firewall", 35 | "opt-firewall-desc": "Nevypínejte, pokud nejde o ladění.", 36 | "opt-customFirewallRules": "Vlastní pravidla firewallu", 37 | "opt-customFirewallRules-desc": "Přepíše výchozí pravidla.", 38 | "opt-transparency-desc": "Zprůhlední okno pro použití průsvitných motiv.", 39 | "opt-button-openGoofCordFolder": "Otevřít složku GoofCord", 40 | "opt-button-loadFromCloud": "Načíst z cloudu", 41 | "opt-button-saveToCloud": "Uložit do cloudu", 42 | "opt-button-deleteCloud": "Smazat cloudová data", 43 | "goofcord-settings": "Otevřít Nastavení", 44 | "screenshare-audio": "Zvuk", 45 | "menu-edit-cut": "Vyjmout", 46 | "menu-edit-selectAll": "Vybrat vše", 47 | "menu-development-gpuDebug": "Otevřít chrome://gpu", 48 | "category-general": "Obecné nastavení", 49 | "menu-edit": "Upravit", 50 | "opt-locale": "Jazyk 🌍", 51 | "opt-locale-desc": "Liší se od jazyka Discordu. Goofcord můžete přeložit zde.", 52 | "welcomeMessage": "Vítejte ke GoofCordu!\nNastavte si nastavení k vaší oblibě a restartujte GoofCord pro přístup k Discordu.\nMůžete to udělat pomocí Ctrl+Shift+R nebo přes nabídky v tácku/doku.\nŠťastné povídání!", 53 | "opt-launchWithOsBoot": "Otevřít GoofCord po spuštění", 54 | "opt-minimizeToTray": "Minimalizovat do tácku", 55 | "opt-cloudHost-desc": "Odkaz na cloudový server GoofCordu. Můžete jej hostovat sami, pro informace navštivte tuto repozitář.", 56 | "opt-updateNotification": "Aktualizační oznámení", 57 | "opt-button-clearCache": "Smazat mezipamět", 58 | "opt-launchWithOsBoot-desc": "GoofCord se automaticky spustí po zapnutí počítače. V některých Linuxových prostředí nemusí fungovat.", 59 | "opt-updateNotification-desc": "Buďte oznámeni o vydání nových verzí.", 60 | "opt-transparency": "Průhlednost", 61 | "opt-cloudHost": "Hostitel Cloudu", 62 | "opt-messageEncryption": "Šifra zpráv", 63 | "menu-edit-paste": "Vložit", 64 | "menu-goofcord-cyclePasswords": "Procházet přes hesla", 65 | "category-cloud": "Nastavení cloudu", 66 | "goofcord-restart": "Restartovat", 67 | "screenshare-optimization": "Optimalizace", 68 | "yes": "Ano", 69 | "opt-messageEncryption-desc": "Pro informace navštivte stránku šifra zpráv.", 70 | "opt-customIconPath-desc": "Vyberte si alternativní ikonu, kterou má GoofCord používat. Doporučují se obrázky s průhledností.", 71 | "opt-encryptionPasswords": "Hesla na šifrování", 72 | "opt-encryptionPasswords-desc": "Bezpečně uložený a zašifrovaný seznam hesel, která budou použita pro šifrování. Doporučuje se zálohování na teplém a bezpečném místě. Zápisy oddělujte čárkami." 73 | } 74 | -------------------------------------------------------------------------------- /assets/lang/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcomeMessage": "Welcome to GoofCord!\nSet up the settings to your liking and close the settings to access Discord.\nHappy chatting!", 3 | "inviteMessage": "Do you want to join", 4 | "yes": "Yes", 5 | "no": "No", 6 | "goofcord-about": "About GoofCord", 7 | "goofcord-quit": "Quit", 8 | "goofcord-open": "Open GoofCord", 9 | "goofcord-settings": "Open Settings", 10 | "goofcord-reload": "Reload", 11 | "goofcord-restart": "Restart", 12 | "goofcord-fullScreen": "Fullscreen", 13 | "menu-goofcord-cyclePasswords": "Cycle through passwords", 14 | "menu-edit": "Edit", 15 | "menu-edit-undo": "Undo", 16 | "menu-edit-redo": "Redo", 17 | "menu-edit-cut": "Cut", 18 | "menu-edit-copy": "Copy", 19 | "menu-edit-paste": "Paste", 20 | "menu-edit-selectAll": "Select all", 21 | "menu-zoom": "Zoom", 22 | "menu-zoom-in": "Zoom in", 23 | "menu-zoom-out": "Zoom out", 24 | "menu-development": "Development", 25 | "menu-development-devtools": "Open DevTools", 26 | "menu-development-gpuDebug": "Open chrome://gpu", 27 | "screenshare-screenshare": "Screen Share", 28 | "screenshare-audio": "Audio", 29 | "screenshare-optimization": "Optimization", 30 | "screenshare-optimization-motion": "Motion", 31 | "screenshare-optimization-detail": "Detail", 32 | "screenshare-resolution": "Resolution", 33 | "screenshare-framerate": "Framerate", 34 | "settingsWindow-title": "GoofCord Settings | Version: ", 35 | "settings-encryption-unavailable": "GoofCord was unable to detect a password manager. Encrypted settings will be stored in plain text. If you are on Linux, install gnome-libsecret or kwallet", 36 | "category-general": "General", 37 | "category-appearance": "Appearance", 38 | "category-client": "Client Mods", 39 | "category-other": "Other", 40 | "category-cloud": "Cloud", 41 | "opt-locale": "Language 🌍", 42 | "opt-locale-desc": "This is different from Discord's language. You can translate GoofCord here.", 43 | "opt-discordUrl": "Discord URL", 44 | "opt-discordUrl-desc": "URL that GoofCord will load on launch. Add \"canary.\" or \"ptb.\" before \"discord.com\" for respective instances.", 45 | "opt-arrpc": "Activity display", 46 | "opt-arrpc-desc": "Enables an open source reimplementation of Discord's\nrich presence called arRPC.\nA workaround is needed for arRPC to work on Flatpak", 47 | "opt-minimizeToTray": "Minimize to tray", 48 | "opt-minimizeToTray-desc": "GoofCord stays open even after closing all windows.", 49 | "opt-startMinimized": "Start minimized", 50 | "opt-startMinimized-desc": "GoofCord starts in the background.", 51 | "opt-launchWithOsBoot": "Launch GoofCord on startup", 52 | "opt-launchWithOsBoot-desc": "Start GoofCord automatically on system boot. May not work in some Linux environments.", 53 | "opt-spellcheck": "Spellcheck", 54 | "opt-spellcheck-desc": "Enables spellcheck for input fields.", 55 | "opt-updateNotification": "Update notification", 56 | "opt-updateNotification-desc": "Get notified about new version releases.", 57 | "opt-customTitlebar": "Custom titlebar", 58 | "opt-customTitlebar-desc": "Enables a Discord-like titlebar.", 59 | "opt-disableAltMenu": "Disable application menu", 60 | "opt-disableAltMenu-desc": "Stops Alt key from opening the app menu.", 61 | "opt-dynamicIcon": "Dynamic icon", 62 | "opt-dynamicIcon-desc": "Shows pings/mentions count on GoofCord's icon and its tray. On Linux, pings on the taskbar only work when unitylib is installed.", 63 | "opt-customIconPath": "Custom Icon", 64 | "opt-customIconPath-desc": "Select an alternative icon for GoofCord to use. Images with transparency are recommended.", 65 | "opt-trayIcon": "Tray icon", 66 | "opt-trayIcon-desc": "What tray icon to use. Symbolic attempts to mimic Gnome's monochromatic icons.", 67 | "opt-autoscroll": "Auto-scroll", 68 | "opt-autoscroll-desc": "Enables auto-scrolling with middle mouse button.", 69 | "opt-popoutWindowAlwaysOnTop": "Pop out window always on top", 70 | "opt-popoutWindowAlwaysOnTop-desc": "Makes voice chat pop out window always stay above other windows.", 71 | "opt-transparency": "Transparency", 72 | "opt-transparency-desc": "Makes the window transparent for use with translucent themes.", 73 | "opt-modNames": "Client mods", 74 | "opt-modNames-desc": "What client mods to use. You shouldn't disable Shelter as it is used by many GoofCord features. Do not mix forks of the same mod (e.g. Vencord and Equicord). Client mod I want to use is not listed.", 75 | "opt-customJsBundle": "Custom JS bundle", 76 | "opt-customJsBundle-desc": "", 77 | "opt-customCssBundle": "Custom CSS bundle", 78 | "opt-customCssBundle-desc": "A raw link to the JS bundle and CSS bundle of a client mod you want to use.", 79 | "opt-invidiousEmbeds": "Invidious embeds", 80 | "opt-invidiousEmbeds-desc": "Replaces YouTube embeds with Invidious embeds. You can customize the instance from Shelter's settings.", 81 | "opt-messageEncryption": "Message encryption", 82 | "opt-messageEncryption-desc": "See message encryption.", 83 | "opt-encryptionPasswords": "Encryption passwords", 84 | "opt-encryptionPasswords-desc": "Securely stored, encrypted list of passwords that will be used for encryption. A backup in a warm, safe place is recommended. Separate entries with commas.", 85 | "opt-encryptionCover": "Encryption cover", 86 | "opt-encryptionCover-desc": "A message that a user without the password will see. At least two words or empty.", 87 | "opt-encryptionMark": "Encryption Mark", 88 | "opt-encryptionMark-desc": "A string that will be prepended to each decrypted message so it's easier to know what messages are encrypted.", 89 | "opt-domOptimizer": "DOM optimizer", 90 | "opt-domOptimizer-desc": "Defers DOM updates to possibly improve performance. May cause visual artifacts.", 91 | "opt-renderingOptimizations": "Rendering optimizations", 92 | "opt-renderingOptimizations-desc": "Applies CSS optimizations to improve scrolling smoothness. May cause text to become blurry if used with some themes.", 93 | "opt-forceDedicatedGPU": "Force dedicated GPU", 94 | "opt-forceDedicatedGPU-desc": "Forces GoofCord to use a dedicated GPU if available.", 95 | "opt-firewall": "Firewall", 96 | "opt-firewall-desc": "Never disable unless for debugging.", 97 | "opt-customFirewallRules": "Custom firewall rules", 98 | "opt-customFirewallRules-desc": "Override the default rules.", 99 | "opt-blocklist": "Blocklist", 100 | "opt-blocklist-desc": "A list of URLs to block. Each entry must be separated by a comma.", 101 | "opt-blockedStrings": "Blocked strings", 102 | "opt-blockedStrings-desc": "If any of specified strings are in the URL, it will be blocked.", 103 | "opt-allowedStrings": "Allowed strings", 104 | "opt-allowedStrings-desc": "If any of specified strings are in the URL, it will *not* be blocked.", 105 | "opt-button-openGoofCordFolder": "Open GoofCord folder", 106 | "opt-button-clearCache": "Clear cache", 107 | "opt-autoSaveCloud": "Auto save", 108 | "opt-autoSaveCloud-desc": "Automatically save settings to cloud when they change.", 109 | "opt-cloudHost": "Cloud Host", 110 | "opt-cloudHost-desc": "GoofCord Cloud Server URL. You can self-host it yourself, see the repository.", 111 | "opt-cloudEncryptionKey": "Cloud Encryption Key", 112 | "opt-cloudEncryptionKey-desc": "Leave empty to not use encryption and not save message encryption passwords on cloud. You can't recover your password if you lose it.", 113 | "opt-button-loadFromCloud": "Load from cloud", 114 | "opt-button-saveToCloud": "Save to cloud", 115 | "opt-button-deleteCloud": "Delete cloud data" 116 | } -------------------------------------------------------------------------------- /assets/lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "goofcord-open": "Ouvrir GoofCord", 3 | "welcomeMessage": "Bienvenue à GoofCord !\nConfigurez les paramètres à votre convenance et redémarrez GoofCord pour accéder à Discord.\nVous pouvez le faire avec Ctrl+Maj+R ou par le menu tray/dock.\nBonne discussion !", 4 | "goofcord-about": "À propos GoofCord", 5 | "goofcord-quit": "Abandon", 6 | "goofcord-settings": "Ouvrir les paramètres", 7 | "screenshare-screenshare": "Partage d'Écran", 8 | "screenshare-audio": "Audio", 9 | "screenshare-optimization": "Optimisation", 10 | "screenshare-optimization-motion": "Mouvement", 11 | "screenshare-optimization-detail": "Détail", 12 | "screenshare-resolution": "Résolution", 13 | "screenshare-framerate": "Fréquence d'images", 14 | "category-other": "Autres paramètres", 15 | "category-cloud": "Paramètres du nuage", 16 | "opt-customTitlebar": "Barre de titre personnalisée", 17 | "opt-customTitlebar-desc": "Active une barre de titre de type Discord.", 18 | "opt-minimizeToTray": "Minimiser dans la zone de notification", 19 | "opt-minimizeToTray-desc": "GoofCord reste ouvert même après avoir fermé toutes les fenêtres.", 20 | "opt-encryptionMark": "Marque de cryptage", 21 | "opt-encryptionMark-desc": "Une chaîne qui sera ajoutée à chaque message décrypté afin qu'il soit plus facile de savoir quels messages sont cryptés.", 22 | "opt-firewall": "Pare-Feu", 23 | "goofcord-reload": "Recharger", 24 | "goofcord-restart": "Redémarrage", 25 | "menu-goofcord-cyclePasswords": "Parcourir les mots de passe", 26 | "goofcord-fullScreen": "Plein écran", 27 | "menu-edit": "Modifier", 28 | "menu-edit-cut": "Couper", 29 | "menu-edit-copy": "Copier", 30 | "category-general": "Paramètres Généraux", 31 | "settingsWindow-title": "Paramètres de Goofcord | Version : ", 32 | "menu-edit-paste": "Coller", 33 | "menu-edit-selectAll": "Tout sélectionner", 34 | "menu-zoom": "Zoom", 35 | "menu-zoom-in": "Zoom avant", 36 | "menu-zoom-out": "Zoom arrière", 37 | "menu-development": "Développement", 38 | "menu-development-devtools": "Ouvrir DevTools", 39 | "menu-development-gpuDebug": "Ouvrir chrome://gpu", 40 | "opt-startMinimized": "Démarrer l'application minimisée", 41 | "opt-customIconPath": "Icône personnalisée", 42 | "opt-customIconPath-desc": "Sélectionnez une autre icône à utiliser par GoofCord. Il est recommandé d'utiliser des images transparentes.", 43 | "opt-discordUrl": "URL Discord", 44 | "opt-discordUrl-desc": "URL que GoofCord chargera au lancement. Ajoutez « canary. » ou « ptb. » avant « discord.com » pour les instances respectives.", 45 | "opt-modNames": "Modifications du client", 46 | "opt-customJsBundle": "Paquet JS personnalisé", 47 | "opt-customCssBundle": "Paquet CSS personnalisé", 48 | "opt-invidiousEmbeds-desc": "Remplace les embeds YouTube par des embeds Invidious. Vous pouvez personnaliser l'instance dans les paramètres de Shelter.", 49 | "opt-messageEncryption": "Chiffrement de message", 50 | "opt-encryptionPasswords": "Mots de passe de cryptage", 51 | "opt-encryptionCover": "Couverture de chiffrement", 52 | "opt-blocklist": "Liste de blocage", 53 | "opt-locale": "Langue 🌍", 54 | "opt-locale-desc": "Cette langue est différente de celle de Discord. Vous pouvez traduire GoofCord ici.", 55 | "opt-startMinimized-desc": "GoofCord démarre en arrière-plan et reste à l'écart.", 56 | "opt-dynamicIcon": "Icône dynamique", 57 | "opt-customCssBundle-desc": "Un lien brut vers le paquet JS et le paquet CSS d'un mod client que vous souhaitez utiliser.", 58 | "opt-encryptionCover-desc": "Un message qu'un utilisateur sans le mot de passe verra. Au moins deux mots ou vide.", 59 | "opt-firewall-desc": "Ne jamais désactiver, sauf pour le débogage.", 60 | "opt-customFirewallRules": "Règles de pare-feu personnalisées", 61 | "opt-customFirewallRules-desc": "Remplacer les règles par défaut.", 62 | "category-client": "Paramètres de modification du client", 63 | "opt-dynamicIcon-desc": "Affiche le nombre de pings/mentions sur l'icône de GoofCord et dans la barre des tâches. Remplace l'icône personnalisée.\nSous Linux, les pings sur l'icône de la barre des tâches n'apparaissent que lorsque le lanceur Unity ou unitylib est utilisé", 64 | "opt-modNames-desc": "Quels mods client utiliser. Vous ne devriez pas désactiver Sheltercar il est utilisé par de nombreuses fonctionnalités de GoofCord. Ne pas mélanger les forks d'un même mod (par exemple Vencord et Equicord). Le mod client que je veux utiliser n'est pas listé.", 65 | "opt-invidiousEmbeds": "Embeds de Invidious", 66 | "opt-messageEncryption-desc": "Voircryptage des messages.", 67 | "opt-encryptionPasswords-desc": "Liste chiffrée et stockée en toute sécurité des mots de passe qui seront utilisés pour le chiffrement. Il est recommandé de conserver une copie de sauvegarde dans un endroit chaud et sûr. Séparez les entrées par des virgules.", 68 | "no": "Non", 69 | "inviteMessage": "Voulez-vous rejoindre", 70 | "yes": "Oui", 71 | "menu-edit-undo": "Annuler", 72 | "menu-edit-redo": "Rétablir", 73 | "opt-blocklist-desc": "Une liste d'URLs à bloquer. Chaque entrée doit être séparée par une virgule.", 74 | "opt-blockedStrings": "Chaînes de caractères bloquées" 75 | } 76 | -------------------------------------------------------------------------------- /assets/lang/nb_NO.json: -------------------------------------------------------------------------------- 1 | { 2 | "goofcord-quit": "Avslutt", 3 | "goofcord-open": "Åpne GoofCord", 4 | "goofcord-settings": "Åpne innstillingene", 5 | "goofcord-reload": "Last inn igjen", 6 | "menu-zoom-in": "Forstørr", 7 | "goofcord-restart": "Programomstart", 8 | "goofcord-fullScreen": "Fullskjermsvining", 9 | "menu-edit-redo": "Gjenta", 10 | "menu-edit-cut": "Klipp ut", 11 | "menu-edit-copy": "Kopier", 12 | "menu-edit-paste": "Lim inn", 13 | "menu-zoom-out": "Forminsk", 14 | "menu-development": "Utvikling", 15 | "menu-edit": "Rediger", 16 | "menu-edit-undo": "Angre", 17 | "menu-edit-selectAll": "Velg alle", 18 | "menu-zoom": "Forstørrelse", 19 | "opt-minimizeToTray": "Minimer til systemkurv", 20 | "opt-minimizeToTray-desc": "GoofCord forblir åpet etter lukking av alle vinduer.", 21 | "opt-startMinimized": "Start minimert", 22 | "opt-dynamicIcon": "Dynamisk ikon", 23 | "opt-discordUrl": "Discord-nettadresse", 24 | "goofcord-about": "Om GoofCord", 25 | "category-other": "Andre innstillinger", 26 | "opt-customIconPath": "Egendefinert ikon", 27 | "opt-messageEncryption": "Meldingskryptering", 28 | "opt-encryptionPasswords": "Krypteringspassord", 29 | "opt-messageEncryption-desc": "Vis meldingskryptering.", 30 | "opt-encryptionCover": "Krypteringsomslag", 31 | "opt-encryptionCover-desc": "En melding en bruker som ikke har passordet kan se. Minst to ord, eller ingenting.", 32 | "opt-customFirewallRules-desc": "Overskrift forvalgte regler.", 33 | "opt-blocklist": "Svarteliste", 34 | "opt-encryptionMark": "Krypteringsmerke", 35 | "opt-button-deleteCloud": "Slett skydata", 36 | "opt-firewall": "Brannmur", 37 | "opt-firewall-desc": "Aldri skru av, med mindre du feilsøker", 38 | "category-cloud": "Skyinnstillinger", 39 | "opt-customFirewallRules": "Egendefinerte brannmursregler", 40 | "category-general": "Generelle innstillinger" 41 | } 42 | -------------------------------------------------------------------------------- /assets/lang/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "goofcord-about": "Over GoofCord", 3 | "goofcord-quit": "Stop", 4 | "goofcord-open": "Open GoofCord", 5 | "goofcord-settings": "Open Instellingen", 6 | "goofcord-reload": "Herladen", 7 | "goofcord-restart": "Herstart", 8 | "goofcord-fullScreen": "Volledig scherm", 9 | "menu-goofcord-cyclePasswords": "Door wachtwoorden scrollen", 10 | "menu-edit": "Bewerken", 11 | "menu-edit-undo": "Ongedaan maken", 12 | "menu-edit-redo": "Heerhai", 13 | "menu-edit-cut": "Snijden", 14 | "menu-edit-paste": "Plak", 15 | "menu-edit-selectAll": "Alles kiezen", 16 | "menu-zoom": "Zoomen", 17 | "welcomeMessage": "Welkom bij GoofCord!\nStel de instellingen naar wens in en start GoofCord opnieuw op om Discord te openen.\nJe kunt dit doen met Ctrl+Shift+R of via het tray/dock menu.\nVeel plezier met chatten!", 18 | "menu-edit-copy": "Kopie", 19 | "menu-zoom-in": "Zoom in", 20 | "menu-zoom-out": "Zoom uit", 21 | "menu-development": "Ontwikkeling", 22 | "menu-development-devtools": "Open DevTools", 23 | "menu-development-gpuDebug": "Open chrome://gpu", 24 | "settingsWindow-title": "GoofCord Instellingen | Versie: ", 25 | "category-general": "Algemene instellingen", 26 | "category-client": "Instellingen client mods" 27 | } 28 | -------------------------------------------------------------------------------- /assets/lang/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenshare-optimization-motion": "Gibanje", 3 | "goofcord-about": "O GoofCordu", 4 | "goofcord-open": "Odpri GoofCord", 5 | "goofcord-quit": "Izhod", 6 | "goofcord-restart": "Ponovni zagon", 7 | "goofcord-fullScreen": "Celotni zaslon", 8 | "menu-edit-cut": "Izreži", 9 | "goofcord-reload": "Ponovno naloži", 10 | "menu-edit": "Uredi", 11 | "menu-edit-undo": "Razveljavi", 12 | "menu-edit-redo": "Uveljavi", 13 | "menu-goofcord-cyclePasswords": "Preklapljaj med gesli", 14 | "menu-development-gpuDebug": "Odpri chrome://gpu", 15 | "screenshare-screenshare": "Deljenje zaslona", 16 | "screenshare-audio": "Zvok", 17 | "menu-zoom": "Povečaj", 18 | "menu-zoom-in": "Približaj", 19 | "menu-zoom-out": "Pomanjšaj", 20 | "menu-development": "Razvoj", 21 | "screenshare-optimization-detail": "Podrobnosti", 22 | "opt-locale": "Jeziki 🌍", 23 | "screenshare-resolution": "Ločljivost", 24 | "screenshare-framerate": "Hitrost sličic", 25 | "category-general": "Splošne nastavitve", 26 | "opt-customTitlebar": "Naslovna vrstica po meri", 27 | "category-cloud": "Nastavitve oblaka", 28 | "opt-startMinimized": "Zaženi minimizirano", 29 | "opt-startMinimized-desc": "GoofCord se zažene v ozadju in ostane izven vaše poti.", 30 | "opt-dynamicIcon": "Dinamična ikona", 31 | "opt-customIconPath": "Prilagojena ikona", 32 | "opt-discordUrl": "URL Discorda", 33 | "opt-discordUrl-desc": "URL, ki ga bo GoofCord naložil ob zagonu. Dodajte \"canary.\" ali \"ptb.\" pred \"discord.com\" za ustrezne različice.", 34 | "opt-customTitlebar-desc": "Omogoči naslovno vrstico, podobno Discordovi.", 35 | "opt-minimizeToTray": "Minimiziraj v sistemski pladenj", 36 | "opt-minimizeToTray-desc": "Discord ostane odprt tudi po zaprtju vseh oken.", 37 | "category-client": "Nastavitve odjemalske modifikacije", 38 | "menu-edit-copy": "Kopiraj", 39 | "menu-edit-paste": "Prilepi", 40 | "goofcord-settings": "Odpri nastavitve", 41 | "menu-edit-selectAll": "Izberi vse", 42 | "screenshare-optimization": "Optimizacija", 43 | "settingsWindow-title": "Nastavitve GoofCord | Verzija: ", 44 | "menu-development-devtools": "Odpri DevTools", 45 | "category-other": "Druge nastavitve", 46 | "opt-locale-desc": "To se razlikuje od Discordovega jezika. GoofCord lahko prevedete tukaj.", 47 | "opt-modNames-desc": "Katere modifikacije odjemalca uporabiti. Ne onemogočite Shelter, saj ga uporablja veliko Discordovih funkcij. Ne mešajte različnih različic iste modifikacije (npr. Vencord in Equicord). Modifikacija odjemalca, ki jo želim uporabiti, ni navedena.", 48 | "opt-customIconPath-desc": "Izberite alternativno ikono, ki jo bo GoofCord uporabil. Priporočljive so slike s prosojnostjo.", 49 | "opt-dynamicIcon-desc": "Prikazuje število omemb/pingov na ikoni GoofCordu in v opravilni vrstici. Prepiše prilagojeno ikono.\nNa Linuxu se pingi na ikoni opravilne vrstice prikažejo samo, če je uporabljen zaganjalnik Unity ali unitylib", 50 | "welcomeMessage": "Dobrodošli v GoofCordu!\nNastavite nastavitve po svoji želji in ponovno zaženite GoofCord za dostop do Discorda.\nTo lahko storite s Ctrl+Shift+R ali prek menija v pladnju/dokih.\nVeselo klepetanje!" 51 | } 52 | -------------------------------------------------------------------------------- /assets/lang/ta.json: -------------------------------------------------------------------------------- 1 | { 2 | "opt-encryptionMark-desc": "மறைகுறியாக்கப்பட்ட ஒவ்வொரு செய்திக்கும் தயாரிக்கப்படும் ஒரு சரம், எனவே என்ன செய்திகள் குறியாக்கம் செய்யப்படுகின்றன என்பதை அறிவது எளிது.", 3 | "opt-firewall": "ஃபயர்வால்", 4 | "opt-customFirewallRules-desc": "இயல்புநிலை விதிகளை மீறவும்.", 5 | "opt-autoscroll": "ஆட்டோ-ச்க்ரோல்", 6 | "opt-minimizeToTray": "தட்டில் குறைக்கவும்", 7 | "opt-customJsBundle-desc": " ", 8 | "welcomeMessage": "கூஃப்கார்டுக்கு வருக!\n உங்கள் விருப்பத்திற்கு அமைப்புகளை அமைத்து, முரண்பாட்டை அணுக கூஃப்கார்டை மறுதொடக்கம் செய்யுங்கள்.\n நீங்கள் இதை Ctrl+Shift+R அல்லது தட்டு/கப்பல்துறை பட்டியல் மூலம் செய்யலாம்.\n இனிய அரட்டை!", 9 | "goofcord-about": "கூஃப்கார்ட் பற்றி", 10 | "goofcord-quit": "வெளியேறு", 11 | "goofcord-open": "Goofcord ஐத் திறக்கவும்", 12 | "goofcord-settings": "திறந்த அமைப்புகள்", 13 | "goofcord-reload": "ஏற்றவும்", 14 | "goofcord-restart": "மறுதொடக்கம்", 15 | "goofcord-fullScreen": "முழு திரை", 16 | "menu-goofcord-cyclePasswords": "கடவுச்சொற்கள் மூலம் சுழற்சி", 17 | "menu-edit": "தொகு", 18 | "menu-edit-undo": "செயல்தவிர்", 19 | "menu-edit-redo": "மீண்டும்செய்", 20 | "menu-edit-cut": "வெட்டு", 21 | "menu-edit-copy": "நகலெடு", 22 | "menu-edit-paste": "ஒட்டு", 23 | "menu-edit-selectAll": "அனைத்தையும் தெரிவுசெய்", 24 | "menu-zoom": "பெரிதாக்கு", 25 | "menu-zoom-in": "பெரிதாக்கு", 26 | "menu-zoom-out": "சிறிதாக்கு", 27 | "menu-development": "வளர்ச்சி", 28 | "menu-development-devtools": "தேவ்டூல்ச் திறந்த", 29 | "menu-development-gpuDebug": "திறந்த Chrome: // GPU", 30 | "screenshare-screenshare": "திரை பகிர்வு", 31 | "screenshare-audio": "ஆடியோ", 32 | "screenshare-optimization": "உகப்பாக்கம்", 33 | "screenshare-optimization-motion": "இயக்கம்", 34 | "screenshare-optimization-detail": "விவரம்", 35 | "screenshare-resolution": "பகுத்தல்", 36 | "screenshare-framerate": "பிரேம்ரேட்", 37 | "settingsWindow-title": "Goofcord அமைப்புகள் | பதிப்பு: ", 38 | "settings-encryption-unavailable": "கடவுச்சொல் நிர்வாகியை கோஃப்கார்டால் கண்டறிய முடியவில்லை. மறைகுறியாக்கப்பட்ட அமைப்புகள் எளிய உரையில் சேமிக்கப்படும். நீங்கள் லினக்சில் இருந்தால், க்னோம்-லிப்செக்ரெட் அல்லது க்வாலட்டை நிறுவவும்", 39 | "category-general": "பொது அமைப்புகள்", 40 | "category-client": "கிளையன்ட் மோட்ச் அமைப்புகள்", 41 | "category-other": "பிற அமைப்புகள்", 42 | "category-cloud": "முகில் அமைப்புகள்", 43 | "opt-locale": "மொழி", 44 | "opt-locale-desc": "இது டிச்கார்ட்டின் மொழியிலிருந்து வேறுபட்டது. நீங்கள் goofcord இங்கே மொழிபெயர்க்கலாம்.", 45 | "opt-customTitlebar": "தனிப்பயன் தலைப்புப் பெட்டி", 46 | "opt-customTitlebar-desc": "டிச்கார்ட் போன்ற தலைப்பு பெட்டியை இயக்குகிறது.", 47 | "opt-minimizeToTray-desc": "அனைத்து சன்னல்களையும் மூடிய பிறகும் கூஃப்கார்ட் திறந்திருக்கும்.", 48 | "opt-startMinimized": "குறைக்கத் தொடங்குங்கள்", 49 | "opt-startMinimized-desc": "கூஃப்கார்ட் பின்னணியில் தொடங்கி உங்கள் வழியிலிருந்து வெளியேறவில்லை.", 50 | "opt-dynamicIcon": "மாறும் படவுரு", 51 | "opt-dynamicIcon-desc": "கூஃப்கார்டின் படவுரு மற்றும் அதன் தட்டில் பிங்ச்/குறிப்புகள் எண்ணிக்கை. தனிப்பயன் ஐகானை மேலெழுதும்.\n பணிப்பட்டு ஐகானில் லினக்ச் பிங்சில் ஒற்றுமை துவக்கி அல்லது யூனிட்டி லிப் பயன்படுத்தப்படும்போது மட்டுமே காண்பிக்கப்படும்", 52 | "opt-customIconPath": "தனிப்பயன் படவுரு", 53 | "opt-customIconPath-desc": "கோஃப்கார்ட் பயன்படுத்த மாற்று ஐகானைத் தேர்ந்தெடுக்கவும். வெளிப்படைத்தன்மை கொண்ட படங்கள் பரிந்துரைக்கப்படுகின்றன.", 54 | "opt-discordUrl": "முரண்பாடு முகவரி", 55 | "opt-discordUrl-desc": "கூஃப்கார்ட் துவக்கத்தில் ஏற்றப்படும் முகவரி. \"கேனரி\" சேர்க்கவும். அல்லது \"PTB.\" அந்தந்த நிகழ்வுகளுக்கு \"டிச்கார்ட்.காம்\" க்கு முன்.", 56 | "opt-modNames": "வாங்கி மோட்ச்", 57 | "opt-modNames-desc": "என்ன கிளையன்ட் மோட்ச் பயன்படுத்த வேண்டும். நீங்கள் தங்குமிடம் முடக்கக்கூடாது இது பல கூஃப்கார்ட் அம்சங்களால் பயன்படுத்தப்படுகிறது. அதே மோடின் முட்கரண்டிகளை கலக்க வேண்டாம் (எ.கா. வென்கார்ட் மற்றும் ஈக்விகார்ட்). நான் பயன்படுத்த விரும்பும் கிளையன்ட் மோட் பட்டியலிடப்படவில்லை .", 58 | "opt-customJsBundle": "தனிப்பயன் JS மூட்டை", 59 | "opt-customCssBundle": "தனிப்பயன் சிஎச்எச் மூட்டை", 60 | "opt-customCssBundle-desc": "நீங்கள் பயன்படுத்த விரும்பும் கிளையன்ட் மோடின் JS மூட்டை மற்றும் சிஎச்எச் மூட்டை ஒரு மூல இணைப்பு.", 61 | "opt-invidiousEmbeds": "ஆவேசமான உட்பொதிகள்", 62 | "opt-invidiousEmbeds-desc": "YouTube உட்பொதிகளை அகங்காத உட்பொதிகளுடன் மாற்றுகிறது. தங்குமிடத்தின் அமைப்புகளிலிருந்து நீங்கள் நிகழ்வைத் தனிப்பயனாக்கலாம்.", 63 | "opt-messageEncryption": "செய்தி குறியாக்கம்", 64 | "opt-messageEncryption-desc": " செய்தி குறியாக்கம் ஐப் பார்க்கவும்.", 65 | "opt-encryptionPasswords": "குறியாக்க கடவுச்சொற்கள்", 66 | "opt-encryptionCover": "குறியாக்க அட்டை", 67 | "opt-encryptionPasswords-desc": "குறியாக்கத்திற்கு பயன்படுத்தப்படும் கடவுச்சொற்களின் பாதுகாப்பாக சேமிக்கப்பட்ட, மறைகுறியாக்கப்பட்ட பட்டியல். சூடான, பாதுகாப்பான இடத்தில் காப்புப்பிரதி பரிந்துரைக்கப்படுகிறது. காற்புள்ளிகளுடன் உள்ளீடுகளை பிரிக்கவும்.", 68 | "opt-encryptionCover-desc": "கடவுச்சொல் இல்லாத பயனர் பார்க்கும் செய்தி. குறைந்தது இரண்டு சொற்கள் அல்லது காலியாக.", 69 | "opt-encryptionMark": "குறியாக்க குறி", 70 | "opt-firewall-desc": "பிழைத்திருத்தத்திற்கு தவிர ஒருபோதும் முடக்க வேண்டாம்.", 71 | "opt-customFirewallRules": "தனிப்பயன் ஃபயர்வால் விதிகள்", 72 | "opt-blocklist": "பிளாக்லிச்ட்", 73 | "opt-blocklist-desc": "தடுக்க முகவரி களின் பட்டியல். ஒவ்வொரு நுழைவும் கமாவால் பிரிக்கப்பட வேண்டும்.", 74 | "opt-blockedStrings": "தடுக்கப்பட்ட சரங்கள்", 75 | "opt-blockedStrings-desc": "குறிப்பிட்ட சரங்கள் ஏதேனும் முகவரி இல் இருந்தால், அது தடுக்கப்படும்.", 76 | "opt-allowedStrings": "அனுமதிக்கப்பட்ட சரங்கள்", 77 | "opt-allowedStrings-desc": "குறிப்பிட்ட சரங்கள் ஏதேனும் முகவரி இல் இருந்தால், அது * தடுக்கப்படாது.", 78 | "opt-arrpc": "செயல்பாட்டு காட்சி", 79 | "opt-arrpc-desc": "டிச்கார்ட்டின் திறந்த மூல மறுசீரமைப்பை செயல்படுத்துகிறது\n <ஒரு இலக்கு = \"_ வெற்று\" href = \"https://github.com/openasar/arpc\"> arrpc என்று அழைக்கப்படும் பணக்கார இருப்பு.\n A 訊息加密。", 60 | "opt-invidiousEmbeds": "Invidious 媒體鑲嵌", 61 | "opt-encryptionCover-desc": "對於其他使用者顯示的加密提示。最少兩個字或是留空。", 62 | "opt-encryptionMark-desc": "在訊息後面新增加密標記文字以得知哪些文字是加密的。", 63 | "opt-firewall-desc": "除了除錯用途外請勿停用此功能。", 64 | "opt-customFirewallRules": "自訂防火牆規則", 65 | "opt-arrpc-desc": "啟用 Discord 內建的動態顯示器(RPC)\n的開源替代品 arRPC。\n對於 GoofCord Flatpak 套件版本需要一些額外步驟以正常啟用此功能", 66 | "opt-spellcheck": "拼字檢查", 67 | "opt-launchWithOsBoot-desc": "在啟動桌面環境時開啟 GoofCord。可能在某些 Linux 環境內無法使用。", 68 | "opt-popoutWindowAlwaysOnTop-desc": "讓語音聊天室視窗在其他視窗之上。", 69 | "opt-popoutWindowAlwaysOnTop": "釘選彈出式視窗", 70 | "opt-autoscroll": "自動捲動", 71 | "opt-autoscroll-desc": "點擊中鍵以啟用自動捲動。", 72 | "opt-transparency": "透明", 73 | "opt-modNames-desc": "使用哪些模組管理器。因為 GoofCord 功能相當依賴 Shelter 請勿停用它。請勿同時混合使用多種同源模組管理器(例如 Vencord 與 Equicord)。我想使用的模組管理器沒有在上面。", 74 | "opt-customCssBundle-desc": "將原始 JS 或 CSS 模組檔案連結貼上。", 75 | "opt-modNames": "模組管理器", 76 | "opt-customJsBundle": "自訂 JS 模組", 77 | "opt-customJsBundle-desc": " ", 78 | "opt-messageEncryption": "訊息加密", 79 | "opt-invidiousEmbeds-desc": "將 YouTube 鑲嵌取代為 Invidious。可於 Shelter 設定中更改。", 80 | "opt-encryptionPasswords": "加密密碼", 81 | "opt-encryptionCover": "加密提示文字", 82 | "opt-blocklist-desc": "網址的黑名單。以逗號分隔不同項目。", 83 | "opt-customFirewallRules-desc": "覆寫預設規則。", 84 | "opt-blocklist": "黑名單", 85 | "opt-blockedStrings-desc": "如果有任何包括此文字的網址將會被封鎖。", 86 | "opt-allowedStrings": "白名單", 87 | "opt-spellcheck-desc": "在輸入欄中檢查拼字錯誤。", 88 | "opt-button-clearCache": "清除快取", 89 | "opt-cloudEncryptionKey": "雲端加密密鑰", 90 | "opt-transparency-desc": "讓視窗透明以使用半透明主題。", 91 | "opt-button-openGoofCordFolder": "開啟 GoofCord 資料夾", 92 | "opt-button-loadFromCloud": "從雲端載入", 93 | "opt-button-saveToCloud": "儲存至雲端", 94 | "opt-button-deleteCloud": "刪除雲端資料", 95 | "opt-domOptimizer": "DOM 優化器", 96 | "inviteMessage": "您想加入嗎", 97 | "no": "不", 98 | "opt-locale": "語言 🌍", 99 | "opt-locale-desc": "這與 Discord 的語言不同。您可以在 這裡 翻譯 GoofCord。", 100 | "opt-cloudHost-desc": "GoofCord 雲端伺服器 URL。您可以自行架設,請參閱 倉庫。", 101 | "opt-cloudHost": "雲端主機", 102 | "yes": "是的", 103 | "opt-domOptimizer-desc": "將 DOM 更新延遲以可能提高性能。可能會導致視覺上的瑕疵。", 104 | "settings-encryption-unavailable": "GoofCord 無法檢測到密碼管理器。加密設定將以純文字形式儲存。如果您使用的是 Linux,請安裝 gnome-libsecret 或 kwallet" 105 | } 106 | -------------------------------------------------------------------------------- /assetsDev/genTestLanguage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const jsonData = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "assets", "lang", "en-US.json"), 'utf8')); 5 | 6 | const result = {}; 7 | 8 | result["TEST"] = "This was autogenerated by assetsDev/genTestLanguage.js"; 9 | 10 | for (const setting in jsonData) { 11 | result[setting] = "💥"; 12 | } 13 | 14 | fs.writeFileSync(path.join(__dirname, "..", "assets", "lang", "test-TEST.json"), JSON.stringify(result, null, 2), 'utf8'); 15 | 16 | console.log('Generated test language'); -------------------------------------------------------------------------------- /assetsDev/gf_install_animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/gf_install_animation.gif -------------------------------------------------------------------------------- /assetsDev/gf_logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/gf_logo_full.png -------------------------------------------------------------------------------- /assetsDev/gf_logo_orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/gf_logo_orig.png -------------------------------------------------------------------------------- /assetsDev/gf_symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 19 | 24 | 29 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /assetsDev/io.github.milkshiift.GoofCord.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.milkshiift.GoofCord 4 | GoofCord 5 | A privacy minded and highly configurable Discord client 6 | 7 | MilkShift 8 | 9 | io.github.milkshiift.GoofCord.desktop 10 | CC0-1.0 11 | OSL-3.0 12 | GoofCord 13 | 14 |
    15 |
  • With Privacy in mind: GoofCord blocks all tracking and uses multiple techniques like message encryption to improve your privacy and security. Learn more on the GitHub wiki
  • 16 |
  • Fast and Performant: Glide through your chats with GoofCord's superior speed and responsiveness compared to the official client.
  • 17 |
  • Standalone: GoofCord is a standalone client, not reliant on the original Discord client in any way.
  • 18 |
  • Plugins & Themes support: Easily use client mods like Vencord, Equicord or Shelter for plugins and themes.
  • 19 |
  • Global Keybinds: Set up keybinds and use them across the system.
  • 20 |
  • Linux support: Seamless screen sharing with audio and native Wayland support on Linux.
  • 21 |
22 |
23 | 24 | 25 | GoofCord with settings open on a themed Kubuntu 26 | https://raw.githubusercontent.com/Milkshiift/GoofCord/939c90f0b8d10d551900cc1aea0458d8a36b45c8/assetsDev/screenshot1.png 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | https://github.com/Milkshiift/GoofCord 50 | https://github.com/Milkshiift/GoofCord/issues 51 | https://github.com/Milkshiift/GoofCord/issues 52 | https://github.com/Milkshiift/GoofCord 53 | 54 | InstantMessaging 55 | AudioVideo 56 | Security 57 | 58 | 59 | always 60 | 61 | 62 | voice 63 | pointing 64 | keyboard 65 | 66 | 67 | intense 68 | intense 69 | intense 70 | intense 71 | 72 | 73 | Discord 74 | Privacy 75 | GoofCord 76 | Mod 77 | Client 78 | Vencord 79 | 80 |
81 | -------------------------------------------------------------------------------- /assetsDev/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/screenshot1.png -------------------------------------------------------------------------------- /assetsDev/screenshot1_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/screenshot1_rounded.png -------------------------------------------------------------------------------- /assetsDev/wikiEncryption1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/wikiEncryption1.png -------------------------------------------------------------------------------- /assetsDev/wikiEncryption2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/wikiEncryption2.png -------------------------------------------------------------------------------- /assetsDev/wikiEncryption3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/wikiEncryption3.png -------------------------------------------------------------------------------- /assetsDev/wikiSceenShare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/assetsDev/wikiSceenShare.png -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "formatter": { 4 | "enabled": false, 5 | "lineWidth": 320 6 | }, 7 | "organizeImports": { 8 | "enabled": true 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true, 14 | "style": { 15 | "useTemplate": "off" 16 | } 17 | } 18 | }, 19 | "files": { 20 | "include": ["src/*"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/build.ts: -------------------------------------------------------------------------------- 1 | // Every script in the "build" directory is meant to be run with Bun 2 | 3 | import fs from "node:fs"; 4 | import path from "node:path"; 5 | import { genIpcHandlers } from "./genIpcHandlers.ts"; 6 | import { genSettingsLangFile } from "./genSettingsLangFile.ts"; 7 | import { generateDTSFile } from "./genSettingsTypes.ts"; 8 | import pc from "picocolors"; 9 | 10 | const isDev = process.argv.some((arg) => arg === "--dev" || arg === "-d"); 11 | 12 | await fs.promises.rm("ts-out", { recursive: true, force: true }); 13 | 14 | console.log("Preprocessing..."); 15 | console.time("dtc"); 16 | await generateDTSFile(); 17 | console.timeEnd("dtc"); 18 | console.time("lang"); 19 | await genSettingsLangFile(); 20 | console.timeEnd("lang"); 21 | console.time("Ipc"); 22 | await genIpcHandlers(); 23 | console.timeEnd("Ipc"); 24 | 25 | console.log("Building..."); 26 | await fs.promises.mkdir("ts-out"); 27 | const bundleResult = await Bun.build({ 28 | minify: true, 29 | sourcemap: isDev ? "linked" : undefined, 30 | format: "esm", 31 | external: ["electron"], 32 | target: "node", 33 | splitting: true, 34 | entrypoints: await searchPreloadFiles("src", [path.join("src", "main.ts")]), 35 | outdir: "ts-out", 36 | packages: "bundle", 37 | }); 38 | if (bundleResult.logs.length) console.log(bundleResult.logs); 39 | 40 | await copyVenmic(); 41 | await copyVenbind(); 42 | 43 | await renamePreloadFiles("./ts-out"); 44 | await fs.promises.cp("./assets/", "./ts-out/assets", { recursive: true }); 45 | 46 | async function searchPreloadFiles(directory: string, result: string[] = []) { 47 | await traverseDirectory(directory, async (filePath: string) => { 48 | if (filePath.endsWith("preload.mts")) { 49 | result.push(filePath); 50 | } 51 | }); 52 | return result; 53 | } 54 | 55 | async function renamePreloadFiles(directoryPath: string) { 56 | await traverseDirectory(directoryPath, async (filePath: string) => { 57 | if (filePath.endsWith("preload.js")) { 58 | const newFilePath = filePath.replace("preload.js", "preload.mjs"); 59 | await fs.promises.rename(filePath, newFilePath); 60 | } 61 | }); 62 | } 63 | 64 | async function traverseDirectory( 65 | directory: string, 66 | fileHandler: (filePath: string) => void, 67 | ) { 68 | const files = await fs.promises.readdir(directory); 69 | 70 | for (const file of files) { 71 | const filePath = path.join(directory, file); 72 | const stats = await fs.promises.stat(filePath); 73 | 74 | if (stats.isDirectory()) { 75 | // Recursively search subdirectories 76 | await traverseDirectory(filePath, fileHandler); 77 | } else { 78 | fileHandler(filePath); 79 | } 80 | } 81 | } 82 | 83 | function copyVenmic() { 84 | if (process.platform !== "linux") return; 85 | 86 | return Promise.all([ 87 | copyFile( 88 | "./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-x64/node-napi-v7.node", 89 | "./assets/venmic-x64.node", 90 | ), 91 | copyFile( 92 | "./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-arm64/node-napi-v7.node", 93 | "./assets/venmic-arm64.node", 94 | ), 95 | ]).catch(() => 96 | console.warn("Failed to copy venmic. Building without venmic support"), 97 | ); 98 | } 99 | 100 | function copyVenbind() { 101 | if (process.platform === "win32") { 102 | return Promise.all([ 103 | copyFile( 104 | "./node_modules/venbind/prebuilds/windows-x86_64/venbind-windows-x86_64.node", 105 | "./assets/venbind-win32-x64.node", 106 | ), 107 | copyFile( 108 | "./node_modules/venbind/prebuilds/windows-aarch64/venbind-windows-aarch64.node", 109 | "./assets/venbind-win32-arm64.node", 110 | ), 111 | ]).catch(() => 112 | console.warn("Failed to copy venbind. Building without venbind support"), 113 | ); 114 | } 115 | 116 | if (process.platform === "linux") { 117 | return Promise.all([ 118 | copyFile( 119 | "./node_modules/venbind/prebuilds/linux-x86_64/venbind-linux-x86_64.node", 120 | "./assets/venbind-linux-x64.node", 121 | ), 122 | copyFile( 123 | "./node_modules/venbind/prebuilds/linux-aarch64/venbind-linux-aarch64.node", 124 | "./assets/venbind-linux-arm64.node", 125 | ), 126 | ]).catch(() => 127 | console.warn("Failed to copy venbind. Building without venbind support"), 128 | ); 129 | } 130 | } 131 | 132 | async function copyFile(src: string, dest: string) { 133 | await Bun.write(dest, Bun.file(src)); 134 | } 135 | 136 | console.log(pc.green("✅ Build completed! 🎉")); 137 | -------------------------------------------------------------------------------- /build/genIpcHandlers.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import path from "node:path"; 3 | import { Glob } from "bun"; 4 | 5 | function isNodeExported(node: ts.Node): boolean { 6 | return ( 7 | // @ts-ignore 8 | node.modifiers?.some( 9 | (modifier: { kind: ts.SyntaxKind; }) => modifier.kind === ts.SyntaxKind.ExportKeyword 10 | ) ?? false 11 | ); 12 | } 13 | 14 | function isTsFunctionAsync(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): boolean { 15 | if (node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword)) { 16 | return true; 17 | } 18 | if (node.type) { 19 | const returnTypeText = node.type.getText(sourceFile); 20 | if (returnTypeText.startsWith("Promise<") || returnTypeText === "Promise") { 21 | return true; 22 | } 23 | } 24 | return false; 25 | } 26 | 27 | function generateHandleHandler(channelName: string, funcName: string, parameters: string, isAsync: boolean): string { 28 | if (isAsync) { 29 | return `ipcMain.handle("${channelName}", async (event, ${parameters}) => { return await ${funcName}(${parameters}); });`; 30 | } else { 31 | return `ipcMain.handle("${channelName}", (event, ${parameters}) => { return ${funcName}(${parameters}); });`; 32 | } 33 | } 34 | 35 | function generateOnHandler(channelName: string, funcName: string, parameters: string, isAsync: boolean): string { 36 | if (isAsync) { 37 | return `ipcMain.on("${channelName}", async (event, ${parameters}) => { event.returnValue = await ${funcName}(${parameters}); });`; 38 | } else { 39 | return `ipcMain.on("${channelName}", (event, ${parameters}) => { event.returnValue = ${funcName}(${parameters}); });`; 40 | } 41 | } 42 | 43 | export async function genIpcHandlers() { 44 | const ipcGenFilePath = "./src/ipcGen.ts"; 45 | const ipcGenFileAbsolutePath = path.resolve(ipcGenFilePath); 46 | const initialContent = "// This file is auto-generated by genIpcHandlers, any changes will be lost\n"; 47 | 48 | const srcRoot = "./src"; 49 | const filePattern = new Glob("**/*.ts"); 50 | const discoveredFilePaths: string[] = []; 51 | 52 | for await (const fileRelative_to_srcRoot of filePattern.scan({ cwd: srcRoot, absolute: false, onlyFiles: true })) { 53 | const projectRelativePath = path.join(srcRoot, fileRelative_to_srcRoot); 54 | const absolutePath = path.resolve(projectRelativePath); 55 | 56 | if (!projectRelativePath.endsWith(".d.ts") && absolutePath !== ipcGenFileAbsolutePath) { 57 | discoveredFilePaths.push(projectRelativePath); 58 | } 59 | } 60 | 61 | const program = ts.createProgram(discoveredFilePaths, { 62 | target: ts.ScriptTarget.ESNext, 63 | module: ts.ModuleKind.CommonJS, 64 | allowJs: true, 65 | }); 66 | 67 | const resolvedDiscoveredPathsSet = new Set(discoveredFilePaths.map(p => path.resolve(p))); 68 | const sourceFilesToProcess = program.getSourceFiles().filter(sf => 69 | !sf.isDeclarationFile && resolvedDiscoveredPathsSet.has(path.resolve(sf.fileName)) 70 | ); 71 | 72 | const collectedImports = new Map>(); 73 | const handlerStatements: string[] = []; 74 | 75 | for (const sourceFile of sourceFilesToProcess) { 76 | const filename = path.basename(sourceFile.fileName, path.extname(sourceFile.fileName)); 77 | 78 | ts.forEachChild(sourceFile, (node) => { 79 | if (ts.isFunctionDeclaration(node) && node.name && isNodeExported(node)) { 80 | const funcName = node.name.getText(sourceFile); 81 | 82 | const typeParametersString = node.typeParameters?.map((tp) => tp.getText(sourceFile)).join(", ") ?? ""; 83 | 84 | if (typeParametersString.includes("IPC")) { 85 | const parameters = node.parameters 86 | .map((param) => param.name.getText(sourceFile)) 87 | .join(", "); 88 | const channelName = `${filename}:${funcName}`; 89 | const isAsync = isTsFunctionAsync(node, sourceFile); 90 | 91 | if (typeParametersString.includes("IPCHandle")) { 92 | handlerStatements.push( 93 | generateHandleHandler(channelName, funcName, parameters, isAsync) 94 | ); 95 | } else if (typeParametersString.includes("IPCOn")) { 96 | handlerStatements.push( 97 | generateOnHandler(channelName, funcName, parameters, isAsync) 98 | ); 99 | } else { 100 | return; 101 | } 102 | 103 | const ipcGenDir = path.dirname(ipcGenFileAbsolutePath); 104 | 105 | let relativeImportPath = path.relative(ipcGenDir, sourceFile.fileName); 106 | relativeImportPath = relativeImportPath.replace(/\\/g, "/").replace(/\.(ts|js|tsx|jsx)$/, ""); 107 | if (!relativeImportPath.startsWith(".")) { 108 | relativeImportPath = "./" + relativeImportPath; 109 | } 110 | 111 | if (!collectedImports.has(relativeImportPath)) { 112 | collectedImports.set(relativeImportPath, new Set()); 113 | } 114 | collectedImports.get(relativeImportPath)!.add(funcName); 115 | } 116 | } 117 | }); 118 | } 119 | 120 | let output = initialContent; 121 | output += `import { ipcMain } from "electron";\n`; 122 | 123 | for (const [moduleSpecifier, namedImports] of collectedImports) { 124 | output += `import { ${Array.from(namedImports).join(", ")} } from "${moduleSpecifier}";\n`; 125 | } 126 | 127 | output += "\n"; 128 | output += handlerStatements.join("\n"); 129 | 130 | await Bun.write(ipcGenFilePath, output); 131 | } 132 | -------------------------------------------------------------------------------- /build/genSettingsLangFile.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { extractKeysAtLevel, extractJSON } from "./cursedJson.ts"; 3 | 4 | export async function genSettingsLangFile() { 5 | interface LangEntries { 6 | [key: string]: string; 7 | } 8 | 9 | function extractNames(data: string): LangEntries { 10 | const result: LangEntries = {}; 11 | 12 | const categories = extractKeysAtLevel(data, 1); 13 | for (const category of categories) { 14 | result[`category-${category.toLowerCase().split(" ")[0]}`] = category; 15 | 16 | const settings = extractJSON(data, [category])!; 17 | const settingKeys = extractKeysAtLevel(settings, 1); 18 | for (const key of settingKeys) { 19 | const name = extractJSON(settings, [key, "name"]); 20 | const description = extractJSON(settings, [key, "description"]); 21 | if (name !== undefined) { 22 | result[`opt-${key}`] = name; 23 | if (description !== undefined) { 24 | result[`opt-${key}-desc`] = description; 25 | } 26 | } 27 | } 28 | } 29 | 30 | return result; 31 | } 32 | 33 | const settingsSchemaPath = path.join( 34 | import.meta.dir, 35 | "..", 36 | "src", 37 | "settingsSchema.ts", 38 | ); 39 | const settingsSchemaFile = Bun.file(settingsSchemaPath); 40 | const fileContent = await settingsSchemaFile.text(); 41 | const file = fileContent.split("settingsSchema = ").pop(); 42 | if (!file) { 43 | console.error("Failed to read settingsSchema file"); 44 | return; 45 | } 46 | 47 | const extractedStrings = extractNames(file); 48 | 49 | const engLangPath = path.join( 50 | import.meta.dir, 51 | "..", 52 | "assets", 53 | "lang", 54 | "en-US.json", 55 | ); 56 | const engLangFile = Bun.file(engLangPath); 57 | const engLang = JSON.parse(await engLangFile.text()); 58 | 59 | for (const key in extractedStrings) { 60 | if (key.startsWith("opt-")) { 61 | delete engLang[key]; 62 | } 63 | engLang[key] = extractedStrings[key]; 64 | 65 | } 66 | 67 | await Bun.write(engLangPath, JSON.stringify(engLang, null, 2)); 68 | } -------------------------------------------------------------------------------- /build/genSettingsTypes.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { extractKeysAtLevel, extractJSON } from "./cursedJson.ts"; 3 | 4 | // Simple mapping of input types to TypeScript types 5 | const TYPE_MAPPING: Record = { 6 | checkbox: "boolean", 7 | textfield: "string", 8 | dropdown: "string", 9 | file: "string", 10 | textarea: "string[]", 11 | "dropdown-multiselect": "string[]" 12 | }; 13 | 14 | function inferType(inputType: string): string { 15 | return TYPE_MAPPING[inputType] ?? "unknown"; 16 | } 17 | 18 | function generateSettingsMappings(data: string): string[] { 19 | const lines: string[] = []; 20 | 21 | const categories = extractKeysAtLevel(data, 1); 22 | for (const category of categories) { 23 | const settings = extractJSON(data, [category])!; 24 | const settingKeys = extractKeysAtLevel(settings, 1); 25 | for (const key of settingKeys) { 26 | if (key.startsWith("button-")) continue; 27 | const outputType = extractJSON(settings, [key, "outputType"]); 28 | const inputType = extractJSON(settings, [key, "inputType"]); 29 | const type = outputType ?? inferType(inputType!); 30 | lines.push(` "${key}": ${type};`); 31 | } 32 | } 33 | 34 | return lines; 35 | } 36 | 37 | function generateType(data: string): string { 38 | const settingKeys: string[] = extractKeysAtLevel(data, 2) 39 | .filter(key => !key.startsWith("button-")) 40 | .map(key => `"${key}"`); 41 | const settingsMappings = generateSettingsMappings(data); 42 | const lines = [ 43 | "// This file is auto-generated. Any changes will be lost. See genSettingsTypes.mjs script", 44 | "", 45 | `export type ConfigKey = ${settingKeys.join(" | ")};`, 46 | "", 47 | "export type ConfigValue = K extends keyof {", 48 | ...settingsMappings, 49 | "} ? {", 50 | ...settingsMappings, 51 | "}[K] : never;", 52 | "", 53 | "export type Config = Map>;" 54 | ]; 55 | 56 | return lines.join("\n"); 57 | } 58 | 59 | const dtsPath = path.join(import.meta.dir, "..", "src", "configTypes.d.ts"); 60 | 61 | export async function generateDTSFile(): Promise { 62 | const file = (await Bun.file(path.join(import.meta.dir, "..", "src", "settingsSchema.ts")).text()) 63 | .split("settingsSchema = ") 64 | .pop()!; 65 | const dtsContent = generateType(file); 66 | await Bun.write(dtsPath, dtsContent); 67 | } -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milkshiift/GoofCord/347a35403012f9658c45c7b8bf0144f7b497405a/build/icon.png -------------------------------------------------------------------------------- /build/installer.nsh: -------------------------------------------------------------------------------- 1 | !macro preInit 2 | SetRegView 64 3 | WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\GoofCord" 4 | WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\GoofCord" 5 | SetRegView 32 6 | WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\GoofCord" 7 | WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\GoofCord" 8 | !macroend -------------------------------------------------------------------------------- /electron-builder.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "electron-builder"; 2 | 3 | export const config: Configuration = { 4 | artifactName: "${productName}-${version}-${os}-${arch}.${ext}", 5 | nsis: { 6 | include: "build/installer.nsh", 7 | artifactName: "${productName} Setup ${arch}.${ext}" 8 | }, 9 | appId: "io.github.milkshiift.GoofCord", 10 | productName: "GoofCord", 11 | files: [ 12 | "!*", 13 | "!node_modules/**/*", 14 | "ts-out", 15 | "package.json", 16 | "LICENSE" 17 | ], 18 | linux: { 19 | icon: "build/icon.icns", 20 | category: "Network", 21 | maintainer: "MilkShift", 22 | target: [ 23 | { 24 | target: "AppImage", 25 | arch: [ 26 | "x64", 27 | "arm64", 28 | "armv7l" 29 | ] 30 | } 31 | ], 32 | executableArgs: [ 33 | "--enable-features=UseOzonePlatform,WaylandWindowDecorations", 34 | "--ozone-platform-hint=auto" 35 | ], 36 | desktop: { 37 | entry: { 38 | Name: "GoofCord", 39 | GenericName: "Internet Messenger", 40 | Type: "Application", 41 | Categories: "Network;InstantMessaging;Chat;", 42 | Keywords: "discord;goofcord;" 43 | } 44 | } 45 | }, 46 | win: { 47 | icon: "build/icon.ico", 48 | target: [ 49 | { 50 | target: "NSIS", 51 | arch: [ 52 | "x64", 53 | "ia32", 54 | "arm64" 55 | ] 56 | } 57 | ] 58 | }, 59 | mac: { 60 | category: "public.app-category.social-networking", 61 | target: [ 62 | { 63 | target: "dmg", 64 | arch: [ 65 | "x64", 66 | "arm64" 67 | ] 68 | } 69 | ], 70 | icon: "build/icon.icns", 71 | extendInfo: { 72 | NSMicrophoneUsageDescription: "This app needs access to the microphone", 73 | NSCameraUsageDescription: "This app needs access to the camera", 74 | "com.apple.security.device.audio-input": true, 75 | "com.apple.security.device.camera": true 76 | } 77 | }, 78 | electronFuses: { 79 | runAsNode: false, 80 | onlyLoadAppFromAsar: true 81 | } 82 | }; 83 | 84 | export default config; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "goofcord", 3 | "version": "1.10.0", 4 | "description": "Take control of your Discord experience with GoofCord – the highly configurable and privacy-minded discord client.", 5 | "main": "ts-out/main.js", 6 | "scripts": { 7 | "build": "bun build/build.ts", 8 | "start": "bun run build --dev && electron ./ts-out/main.js --dev --enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto", 9 | "startWithLoginFix": "bun run fixLogin && bun run start", 10 | "packageLinux": "bun run build && electron-builder --linux", 11 | "packageWindows": "bun run build && electron-builder --win", 12 | "packageMac": "bun run build && electron-builder --mac", 13 | "fixLogin": "rm -rf ~/.config/Electron/Local\\ Storage && cp -r ~/.config/goofcord/Local\\ Storage ~/.config/Electron/" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/Milkshiift/GoofCord" 18 | }, 19 | "author": "MilkShift", 20 | "license": "OSL-3.0", 21 | "bugs": { 22 | "url": "https://github.com/Milkshiift/GoofCord/issues" 23 | }, 24 | "homepage": "https://github.com/Milkshiift/GoofCord", 25 | "devDependencies": { 26 | "@biomejs/biome": "^1.9.4", 27 | "@types/bun": "^1.2.15", 28 | "@types/node": "^22.15.29", 29 | "@types/stegcloak": "^1.0.2", 30 | "@vencord/types": "^1.11.5", 31 | "electron": "^36.3.2", 32 | "electron-builder": "^26.0.12", 33 | "source-map-support": "github:onigoetz/node-source-map-support", 34 | "typescript": "^5.8.3" 35 | }, 36 | "dependencies": { 37 | "arrpc": "github:Milkshiift/arrpc", 38 | "auto-launch": "github:Teamwork/node-auto-launch", 39 | "electron-context-menu": "github:Milkshiift/electron-context-menu", 40 | "picocolors": "^1.1.1", 41 | "stegcloak": "github:Milkshiift/stegcloak-node", 42 | "v8-compile-cache": "github:JulioC/v8-compile-cache#bugfix/esm" 43 | }, 44 | "optionalDependencies": { 45 | "@vencord/venmic": "^6.1.0", 46 | "venbind": "0.1.6" 47 | }, 48 | "trustedDependencies": [ 49 | "@biomejs/biome", 50 | "bufferutil", 51 | "electron", 52 | "esbuild", 53 | "arrpc" 54 | ], 55 | "engines": { 56 | "node": ">=21.0.0" 57 | }, 58 | "type": "module" 59 | } 60 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { app, dialog, shell } from "electron"; 4 | import type { Config, ConfigKey, ConfigValue } from "./configTypes.d.ts"; 5 | import { settingsSchema } from "./settingsSchema.ts"; 6 | import { getErrorMessage, getGoofCordFolderPath, tryCreateFolder } from "./utils.ts"; 7 | 8 | export let cachedConfig: Config; 9 | export let firstLaunch = false; 10 | 11 | async function handleConfigError(e: unknown) { 12 | if (e instanceof Error && "code" in e && e.code === "ENOENT") { 13 | // Config file does not exist 14 | tryCreateFolder(getGoofCordFolderPath()); 15 | await setup(); 16 | } else { 17 | console.error("Failed to load the config:", e); 18 | 19 | await app.whenReady(); 20 | 21 | const buttonId = dialog.showMessageBoxSync({ 22 | type: "question", 23 | buttons: ["Try again", "Open config folder", "Reset config", "Exit"], 24 | defaultId: 0, 25 | title: "Failed to load configuration", 26 | message: `Config loader errored:\n${getErrorMessage(e)}`, 27 | }); 28 | 29 | switch (buttonId) { 30 | case 1: 31 | await shell.openPath(getGoofCordFolderPath()); 32 | break; 33 | case 2: 34 | await setup(); 35 | break; 36 | case 3: 37 | app.exit(); 38 | return true; 39 | } 40 | } 41 | } 42 | 43 | export async function loadConfig(): Promise { 44 | do { 45 | try { 46 | // fs.promises.readFile is much slower than fs.readFileSync 47 | const rawData = fs.readFileSync(getConfigLocation(), "utf-8"); 48 | 49 | cachedConfig = new Map(Object.entries(JSON.parse(rawData))) as Config; 50 | return; // Success, exit the function 51 | } catch (e: unknown) { 52 | const shouldExit = await handleConfigError(e); 53 | if (shouldExit) break; 54 | } 55 | } while (!cachedConfig); 56 | } 57 | 58 | export function getConfig(key: K, bypassDefault = false): ConfigValue { 59 | const value = cachedConfig.get(key as ConfigKey); 60 | if (value !== undefined || bypassDefault) return value as ConfigValue; 61 | 62 | console.log("Missing config parameter:", key); 63 | const defaultValue = getDefaultValue(key); 64 | void setConfig(key, defaultValue); 65 | return defaultValue; 66 | } 67 | 68 | export function getConfigBulk() { 69 | return cachedConfig; 70 | } 71 | 72 | export async function setConfig(entry: K, value: ConfigValue) { 73 | try { 74 | cachedConfig.set(entry, value); 75 | const toSave = JSON.stringify(Object.fromEntries(cachedConfig), undefined, 2); 76 | await fs.promises.writeFile(getConfigLocation(), toSave, "utf-8"); 77 | } catch (e: unknown) { 78 | console.error("setConfig function errored:", e); 79 | dialog.showErrorBox("GoofCord was unable to save the settings", getErrorMessage(e)); 80 | } 81 | } 82 | 83 | export async function setConfigBulk(toSet: Config) { 84 | try { 85 | cachedConfig = toSet; 86 | const toSave = JSON.stringify(Object.fromEntries(cachedConfig), undefined, 2); 87 | await fs.promises.writeFile(getConfigLocation(), toSave, "utf-8"); 88 | } catch (e: unknown) { 89 | console.error("setConfigBulk function errored:", e); 90 | dialog.showErrorBox("GoofCord was unable to save the settings", getErrorMessage(e)); 91 | } 92 | } 93 | 94 | export async function setup() { 95 | console.log("Setting up default GoofCord settings."); 96 | firstLaunch = true; 97 | await setConfigBulk(getDefaults()); 98 | } 99 | 100 | const defaults: Config = new Map(); 101 | export function getDefaults(): Config { 102 | if (defaults.size > 0) return defaults; // Caching 103 | 104 | for (const category in settingsSchema) { 105 | const categorySettings = settingsSchema[category]; 106 | for (const setting in categorySettings) { 107 | defaults.set(setting as ConfigKey, categorySettings[setting].defaultValue); 108 | } 109 | } 110 | 111 | return defaults; 112 | } 113 | 114 | export function getDefaultValue(entry: K): ConfigValue; 115 | export function getDefaultValue(entry: string): unknown; 116 | export function getDefaultValue(entry: string): unknown { 117 | return getDefaults().get(entry as ConfigKey); 118 | } 119 | 120 | export function getConfigLocation(): string { 121 | return path.join(getGoofCordFolderPath(), "settings.json"); 122 | } 123 | -------------------------------------------------------------------------------- /src/configTypes.d.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated. Any changes will be lost. See genSettingsTypes.mjs script 2 | 3 | export type ConfigKey = "locale" | "discordUrl" | "arrpc" | "minimizeToTray" | "startMinimized" | "launchWithOsBoot" | "spellcheck" | "updateNotification" | "customTitlebar" | "disableAltMenu" | "dynamicIcon" | "customIconPath" | "trayIcon" | "autoscroll" | "popoutWindowAlwaysOnTop" | "transparency" | "modNames" | "modEtagCache" | "customJsBundle" | "customCssBundle" | "noBundleUpdates" | "installDefaultShelterPlugins" | "invidiousEmbeds" | "messageEncryption" | "encryptionPasswords" | "encryptionCover" | "encryptionMark" | "domOptimizer" | "renderingOptimizations" | "forceDedicatedGPU" | "firewall" | "customFirewallRules" | "blocklist" | "blockedStrings" | "allowedStrings" | "screensharePreviousSettings" | "windowState:main" | "autoSaveCloud" | "cloudHost" | "cloudToken" | "cloudEncryptionKey"; 4 | 5 | export type ConfigValue = K extends keyof { 6 | "locale": string; 7 | "discordUrl": string; 8 | "arrpc": boolean; 9 | "minimizeToTray": boolean; 10 | "startMinimized": boolean; 11 | "launchWithOsBoot": boolean; 12 | "spellcheck": boolean; 13 | "updateNotification": boolean; 14 | "customTitlebar": boolean; 15 | "disableAltMenu": boolean; 16 | "dynamicIcon": boolean; 17 | "customIconPath": string; 18 | "trayIcon": string; 19 | "autoscroll": boolean; 20 | "popoutWindowAlwaysOnTop": boolean; 21 | "transparency": boolean; 22 | "modNames": string[]; 23 | "modEtagCache": object; 24 | "customJsBundle": string; 25 | "customCssBundle": string; 26 | "noBundleUpdates": boolean; 27 | "installDefaultShelterPlugins": boolean; 28 | "invidiousEmbeds": boolean; 29 | "messageEncryption": boolean; 30 | "encryptionPasswords": string[]; 31 | "encryptionCover": string; 32 | "encryptionMark": string; 33 | "domOptimizer": boolean; 34 | "renderingOptimizations": boolean; 35 | "forceDedicatedGPU": boolean; 36 | "firewall": boolean; 37 | "customFirewallRules": boolean; 38 | "blocklist": string[]; 39 | "blockedStrings": string[]; 40 | "allowedStrings": string[]; 41 | "screensharePreviousSettings": [number, number, boolean, string]; 42 | "windowState:main": [boolean, [number, number]]; 43 | "autoSaveCloud": boolean; 44 | "cloudHost": string; 45 | "cloudToken": string; 46 | "cloudEncryptionKey": string; 47 | } ? { 48 | "locale": string; 49 | "discordUrl": string; 50 | "arrpc": boolean; 51 | "minimizeToTray": boolean; 52 | "startMinimized": boolean; 53 | "launchWithOsBoot": boolean; 54 | "spellcheck": boolean; 55 | "updateNotification": boolean; 56 | "customTitlebar": boolean; 57 | "disableAltMenu": boolean; 58 | "dynamicIcon": boolean; 59 | "customIconPath": string; 60 | "trayIcon": string; 61 | "autoscroll": boolean; 62 | "popoutWindowAlwaysOnTop": boolean; 63 | "transparency": boolean; 64 | "modNames": string[]; 65 | "modEtagCache": object; 66 | "customJsBundle": string; 67 | "customCssBundle": string; 68 | "noBundleUpdates": boolean; 69 | "installDefaultShelterPlugins": boolean; 70 | "invidiousEmbeds": boolean; 71 | "messageEncryption": boolean; 72 | "encryptionPasswords": string[]; 73 | "encryptionCover": string; 74 | "encryptionMark": string; 75 | "domOptimizer": boolean; 76 | "renderingOptimizations": boolean; 77 | "forceDedicatedGPU": boolean; 78 | "firewall": boolean; 79 | "customFirewallRules": boolean; 80 | "blocklist": string[]; 81 | "blockedStrings": string[]; 82 | "allowedStrings": string[]; 83 | "screensharePreviousSettings": [number, number, boolean, string]; 84 | "windowState:main": [boolean, [number, number]]; 85 | "autoSaveCloud": boolean; 86 | "cloudHost": string; 87 | "cloudToken": string; 88 | "cloudEncryptionKey": string; 89 | }[K] : never; 90 | 91 | export type Config = Map>; -------------------------------------------------------------------------------- /src/ipcGen.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated by genIpcHandlers, any changes will be lost 2 | import { ipcMain } from "electron"; 3 | import { getVersion, getDisplayVersion, getAsset, isEncryptionAvailable, encryptSafeStorage, decryptSafeStorage, saveFileToGCFolder } from "./utils"; 4 | import { loadConfig, getConfig, getConfigBulk, setConfig, setConfigBulk, getDefaultValue } from "./config"; 5 | import { i } from "./modules/localization"; 6 | import { stopVenmic } from "./modules/venmic"; 7 | import { encryptMessage, decryptMessage, cycleThroughPasswords } from "./modules/messageEncryption"; 8 | import { loadCloud, saveCloud, deleteCloud } from "./windows/settings/cloud/cloud"; 9 | import { createSettingsWindow, hotreloadLocale } from "./windows/settings/main"; 10 | import { initArrpc } from "./modules/arrpc"; 11 | import { getAssets } from "./modules/assetLoader"; 12 | import { updateModsFull } from "./modules/mods"; 13 | import { clearCache } from "./modules/cacheManager"; 14 | import { setBadgeCount } from "./modules/dynamicIcon"; 15 | import { setKeybinds, isVenbindLoaded } from "./modules/venbind"; 16 | import { setAutoLaunchState } from "./loader"; 17 | 18 | ipcMain.on("utils:getVersion", (event, ) => { event.returnValue = getVersion(); }); 19 | ipcMain.on("utils:getDisplayVersion", (event, ) => { event.returnValue = getDisplayVersion(); }); 20 | ipcMain.on("utils:getAsset", (event, assetName) => { event.returnValue = getAsset(assetName); }); 21 | ipcMain.on("utils:isEncryptionAvailable", (event, ) => { event.returnValue = isEncryptionAvailable(); }); 22 | ipcMain.on("utils:encryptSafeStorage", (event, plaintextString) => { event.returnValue = encryptSafeStorage(plaintextString); }); 23 | ipcMain.on("utils:decryptSafeStorage", (event, encryptedBase64) => { event.returnValue = decryptSafeStorage(encryptedBase64); }); 24 | ipcMain.handle("utils:saveFileToGCFolder", async (event, filePath, content) => { return await saveFileToGCFolder(filePath, content); }); 25 | ipcMain.handle("config:loadConfig", async (event, ) => { return await loadConfig(); }); 26 | ipcMain.on("config:getConfig", (event, key, bypassDefault) => { event.returnValue = getConfig(key, bypassDefault); }); 27 | ipcMain.on("config:getConfigBulk", (event, ) => { event.returnValue = getConfigBulk(); }); 28 | ipcMain.handle("config:setConfig", async (event, entry, value) => { return await setConfig(entry, value); }); 29 | ipcMain.handle("config:setConfigBulk", async (event, toSet) => { return await setConfigBulk(toSet); }); 30 | ipcMain.on("config:getDefaultValue", (event, entry) => { event.returnValue = getDefaultValue(entry); }); 31 | ipcMain.on("localization:i", (event, key) => { event.returnValue = i(key); }); 32 | ipcMain.handle("venmic:stopVenmic", (event, ) => { return stopVenmic(); }); 33 | ipcMain.on("messageEncryption:encryptMessage", (event, message) => { event.returnValue = encryptMessage(message); }); 34 | ipcMain.on("messageEncryption:decryptMessage", (event, message) => { event.returnValue = decryptMessage(message); }); 35 | ipcMain.handle("messageEncryption:cycleThroughPasswords", (event, ) => { return cycleThroughPasswords(); }); 36 | ipcMain.handle("cloud:loadCloud", async (event, ) => { return await loadCloud(); }); 37 | ipcMain.handle("cloud:saveCloud", async (event, silent) => { return await saveCloud(silent); }); 38 | ipcMain.handle("cloud:deleteCloud", async (event, ) => { return await deleteCloud(); }); 39 | ipcMain.handle("main:createSettingsWindow", async (event, ) => { return await createSettingsWindow(); }); 40 | ipcMain.handle("main:hotreloadLocale", async (event, ) => { return await hotreloadLocale(); }); 41 | ipcMain.handle("arrpc:initArrpc", async (event, ) => { return await initArrpc(); }); 42 | ipcMain.on("assetLoader:getAssets", (event, ) => { event.returnValue = getAssets(); }); 43 | ipcMain.handle("mods:updateModsFull", async (event, ) => { return await updateModsFull(); }); 44 | ipcMain.handle("cacheManager:clearCache", async (event, ) => { return await clearCache(); }); 45 | ipcMain.handle("dynamicIcon:setBadgeCount", async (event, count) => { return await setBadgeCount(count); }); 46 | ipcMain.handle("venbind:setKeybinds", async (event, keybinds) => { return await setKeybinds(keybinds); }); 47 | ipcMain.handle("venbind:isVenbindLoaded", async (event, ) => { return await isVenbindLoaded(); }); 48 | ipcMain.handle("loader:setAutoLaunchState", async (event, ) => { return await setAutoLaunchState(); }); -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import { app, dialog, net, session, systemPreferences } from "electron"; 2 | import pc from "picocolors"; 3 | import { firstLaunch, getConfig } from "./config.ts"; 4 | import { setMenu } from "./menu.ts"; 5 | import { initArrpc } from "./modules/arrpc.ts"; 6 | import { categorizeAllAssets, startStyleWatcher } from "./modules/assetLoader.ts"; 7 | import { initFirewall, unstrictCSP } from "./modules/firewall.ts"; 8 | import { i } from "./modules/localization.ts"; 9 | import { initEncryption } from "./modules/messageEncryption.ts"; 10 | import { manageMods, updateMods } from "./modules/mods.ts"; 11 | import { checkForUpdate } from "./modules/updateCheck.ts"; 12 | import { createTray } from "./tray.ts"; 13 | import { getCustomIcon, isDev } from "./utils.ts"; 14 | import { createMainWindow } from "./windows/main/main.ts"; 15 | import { createSettingsWindow } from "./windows/settings/main.ts"; 16 | 17 | export async function load() { 18 | void setAutoLaunchState(); 19 | void setMenu(); 20 | void createTray(); 21 | const preReady = Promise.all([import("./ipcGen.ts"), manageMods().then(() => categorizeAllAssets())]); 22 | 23 | console.time(pc.green("[Timer]") + " Electron loaded in"); 24 | await app.whenReady(); 25 | console.timeEnd(pc.green("[Timer]") + " Electron loaded in"); 26 | 27 | initEncryption(); 28 | initFirewall(); 29 | await Promise.all([preReady, waitForInternetConnection(), setPermissions(), unstrictCSP()]); 30 | firstLaunch ? await handleFirstLaunch() : await createMainWindow(); 31 | 32 | console.timeEnd(pc.green("[Timer]") + " GoofCord fully loaded in"); 33 | 34 | void updateMods(); 35 | void checkForUpdate(); 36 | void initArrpc(); 37 | void startStyleWatcher(); 38 | 39 | if (process.argv.some((arg) => arg === "--settings")) void createSettingsWindow(); 40 | } 41 | 42 | async function waitForInternetConnection() { 43 | while (!net.isOnline()) { 44 | await new Promise((resolve) => setTimeout(resolve, 1000)); 45 | } 46 | } 47 | 48 | async function handleFirstLaunch() { 49 | await createSettingsWindow(); 50 | await new Promise((resolve) => setTimeout(resolve, 1000)); 51 | await dialog.showMessageBox({ 52 | message: i("welcomeMessage"), 53 | type: "info", 54 | icon: getCustomIcon(), 55 | noLink: false, 56 | }); 57 | app.relaunch(); // Relaunches only when user closes settings window 58 | } 59 | 60 | export async function setAutoLaunchState() { 61 | const { default: AutoLaunch } = await import("auto-launch"); 62 | const isAUR = process.execPath.endsWith("electron") && !isDev(); 63 | const gfAutoLaunch = new AutoLaunch({ 64 | name: "GoofCord", 65 | path: isAUR ? "/bin/goofcord" : undefined, 66 | }); 67 | 68 | getConfig("launchWithOsBoot") ? await gfAutoLaunch.enable() : await gfAutoLaunch.disable(); 69 | } 70 | 71 | async function setPermissions() { 72 | session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { 73 | if (process.platform === "darwin" && "mediaTypes" in details) { 74 | if (details.mediaTypes?.includes("audio")) { 75 | callback(await systemPreferences.askForMediaAccess("microphone")); 76 | } 77 | if (details.mediaTypes?.includes("video")) { 78 | callback(await systemPreferences.askForMediaAccess("camera")); 79 | } 80 | } else if (["media", "notifications", "fullscreen", "clipboard-sanitized-write", "openExternal", "pointerLock", "keyboardLock"].includes(permission)) { 81 | callback(true); 82 | } 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { app } from "electron"; 2 | import "v8-compile-cache"; 3 | import pc from "picocolors"; 4 | import { getConfig, loadConfig } from "./config.ts"; 5 | import { initLocalization } from "./modules/localization.ts"; 6 | import { getDisplayVersion, isDev } from "./utils.ts"; 7 | 8 | console.time(pc.green("[Timer]") + " GoofCord fully loaded in"); 9 | 10 | if (isDev()) { 11 | require("source-map-support").install(); 12 | } 13 | 14 | console.log("GoofCord", getDisplayVersion()); 15 | 16 | /* 17 | ! Do not use getConfig or i (localization) in this file 18 | */ 19 | setFlags(); 20 | if (!app.requestSingleInstanceLock()) app.exit(); 21 | 22 | async function main() { 23 | await loadConfig(); 24 | await initLocalization(); 25 | 26 | // Not in loader.ts because it may load too late 27 | if (getConfig("forceDedicatedGPU")) app.commandLine.appendSwitch("force_high_performance_gpu"); 28 | 29 | const loader = await import("./loader"); 30 | await loader.load(); 31 | } 32 | 33 | function setFlags() { 34 | if (process.argv.some((arg) => arg === "--no-flags")) return; 35 | 36 | const disableFeatures = [ 37 | "UseChromeOSDirectVideoDecoder", 38 | "HardwareMediaKeyHandling", 39 | "MediaSessionService", 40 | "WebRtcAllowInputVolumeAdjustment", 41 | "Vulkan", 42 | ]; 43 | const enableFeatures = [ 44 | "WebRTC", 45 | "WebRtcHideLocalIpsWithMdns", 46 | "PlatformHEVCEncoderSupport", 47 | "EnableDrDc", 48 | "CanvasOopRasterization", 49 | "UseSkiaRenderer", 50 | ]; 51 | 52 | if (process.platform === "linux") { 53 | enableFeatures.push("PulseaudioLoopbackForScreenShare"); 54 | if (!process.argv.some((arg) => arg === "--no-vaapi")) { 55 | enableFeatures.push( 56 | "AcceleratedVideoDecodeLinuxGL", 57 | "AcceleratedVideoEncoder", 58 | "AcceleratedVideoDecoder", 59 | "AcceleratedVideoDecodeLinuxZeroCopyGL", 60 | ); 61 | } 62 | } 63 | 64 | const switches = [ 65 | ["enable-gpu-rasterization"], 66 | ["enable-zero-copy"], 67 | ["disable-low-res-tiling"], 68 | ["disable-site-isolation-trials"], 69 | ["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"], 70 | ["autoplay-policy", "no-user-gesture-required"], 71 | ["enable-speech-dispatcher"], 72 | ["disable-http-cache"], 73 | ["disable-features", disableFeatures.join(",")], 74 | ["enable-features", enableFeatures.join(",")], 75 | ]; 76 | 77 | for (const [key, val] of switches) { 78 | app.commandLine.appendSwitch(key, val); 79 | } 80 | } 81 | 82 | main().catch(console.error); 83 | -------------------------------------------------------------------------------- /src/menu.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu } from "electron"; 2 | import contextMenu from "electron-context-menu"; 3 | import { i } from "./modules/localization.ts"; 4 | import { cycleThroughPasswords } from "./modules/messageEncryption.ts"; 5 | import { saveState } from "./modules/windowStateManager.ts"; 6 | import { mainWindow } from "./windows/main/main.ts"; 7 | import { createSettingsWindow } from "./windows/settings/main.ts"; 8 | 9 | export async function setMenu() { 10 | void setApplicationMenu(); 11 | setContextMenu(); 12 | } 13 | 14 | export async function setApplicationMenu() { 15 | const template: Electron.MenuItemConstructorOptions[] = [ 16 | { 17 | label: "GoofCord", 18 | submenu: [ 19 | { label: i("goofcord-about"), role: "about" }, 20 | { type: "separator" }, 21 | { 22 | label: i("goofcord-settings"), 23 | accelerator: "CmdOrCtrl+Shift+'", 24 | click: () => { 25 | createSettingsWindow(); 26 | }, 27 | }, 28 | { 29 | label: i("menu-goofcord-cyclePasswords"), 30 | accelerator: "F9", 31 | click: async () => { 32 | cycleThroughPasswords(); 33 | }, 34 | }, 35 | { 36 | label: i("goofcord-reload"), 37 | accelerator: "CmdOrCtrl+R", 38 | click: async () => { 39 | mainWindow.reload(); 40 | }, 41 | }, 42 | { 43 | label: i("goofcord-restart"), 44 | accelerator: "Shift+CmdOrCtrl+R", 45 | click: async () => { 46 | await saveState(mainWindow, "windowState:main"); 47 | app.relaunch(); 48 | app.exit(); 49 | }, 50 | }, 51 | { 52 | label: i("goofcord-fullScreen"), 53 | role: "togglefullscreen", 54 | }, 55 | { 56 | label: i("goofcord-quit"), 57 | accelerator: "CmdOrCtrl+Q", 58 | click: async () => { 59 | await saveState(mainWindow, "windowState:main"); 60 | app.exit(); 61 | }, 62 | }, 63 | ], 64 | }, 65 | { 66 | label: i("menu-edit"), 67 | submenu: [ 68 | { label: i("menu-edit-undo"), accelerator: "CmdOrCtrl+Z", role: "undo" }, 69 | { label: i("menu-edit-redo"), accelerator: "Shift+CmdOrCtrl+Z", role: "redo" }, 70 | { type: "separator" }, 71 | { label: i("menu-edit-cut"), accelerator: "CmdOrCtrl+X", role: "cut" }, 72 | { label: i("menu-edit-copy"), accelerator: "CmdOrCtrl+C", role: "copy" }, 73 | { label: i("menu-edit-paste"), accelerator: "CmdOrCtrl+V", role: "paste" }, 74 | { label: i("menu-edit-selectAll"), accelerator: "CmdOrCtrl+A", role: "selectAll" }, 75 | ], 76 | }, 77 | { 78 | label: i("menu-zoom"), 79 | submenu: [ 80 | // Fix for zoom in on keyboards with dedicated + like QWERTZ (or numpad) 81 | // See https://github.com/electron/electron/issues/14742 and https://github.com/electron/electron/issues/5256 82 | { label: i("menu-zoom-in"), accelerator: "CmdOrCtrl+Plus", role: "zoomIn", visible: false }, 83 | { label: i("menu-zoom-in"), accelerator: "CmdOrCtrl++", role: "zoomIn" }, 84 | { label: i("menu-zoom-in"), accelerator: "CmdOrCtrl+=", role: "zoomIn", visible: false }, 85 | { label: i("menu-zoom-out"), accelerator: "CmdOrCtrl+-", role: "zoomOut" }, 86 | ], 87 | }, 88 | { 89 | label: i("menu-development"), 90 | submenu: [ 91 | { 92 | label: i("menu-development-devtools"), 93 | accelerator: "CmdOrCtrl+Shift+I", 94 | click: () => { 95 | BrowserWindow.getFocusedWindow()?.webContents.toggleDevTools(); 96 | }, 97 | }, 98 | { 99 | label: i("menu-development-gpuDebug"), 100 | accelerator: "CmdorCtrl+Alt+G", 101 | click() { 102 | const gpuWindow = new BrowserWindow({ 103 | width: 900, 104 | height: 700, 105 | useContentSize: true, 106 | title: "GPU Internals", 107 | }); 108 | gpuWindow.loadURL("chrome://gpu"); 109 | }, 110 | }, 111 | ], 112 | }, 113 | ]; 114 | 115 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 116 | } 117 | 118 | export function setContextMenu() { 119 | contextMenu({ 120 | showSelectAll: true, 121 | showSaveImageAs: true, 122 | showCopyImage: true, 123 | showCopyImageAddress: true, 124 | showCopyLink: true, 125 | showSaveLinkAs: true, 126 | showInspectElement: true, 127 | showSearchWithGoogle: true, 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /src/modules/agent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/SpacingBat3/WebCord 3 | 4 | MIT License 5 | 6 | Copyright (c) 2020-2023 Dawid Papiewski "SpacingBat3" 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | import { release } from "node:os"; 27 | 28 | interface AgentReplace { 29 | platform: string; 30 | version: string; 31 | arch: string; 32 | device?: string | undefined; 33 | } 34 | 35 | const agentArchMap = Object.freeze({ 36 | arm64: "aarch64", 37 | arm: "armv7", 38 | ia32: "x86", 39 | x64: "x86_64", 40 | } as const); 41 | 42 | const toAgentCase = (s: string) => (s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase()).replace("bsd", "BSD").replace("os", "OS"); 43 | 44 | /** 45 | * Generates fake Chrome/Chromium user agent string to use instead Electron ones. 46 | * 47 | * This way, pages identifies Electron client as regular Chromium browser. 48 | * 49 | * To make it even harder to detect, it even uses current operating system version in 50 | * the user agent string (via `process.getSystemVersion()` in Electron API). 51 | * 52 | * @param chromeVersion Chrome/Chromium version string to use. 53 | * @param mobile Whenever user-agent should be for mobile devices. 54 | * @param replace Generate user-agent from provided `replace` data. 55 | * @returns Fake Chrome/Chromium user agent string. 56 | */ 57 | export function getUserAgent(chromeVersion: string, mobile?: boolean, replace?: AgentReplace) { 58 | const userAgentPlatform = replace?.platform ?? process.platform; 59 | const osVersion = replace?.version ?? (typeof process.getSystemVersion === "function" ? process.getSystemVersion() : userAgentPlatform === "darwin" ? "13.5.2" : release()); 60 | const device = replace?.device !== undefined ? (`; ${replace.device}` as const) : ""; 61 | const mobileAgent = (mobile ?? false) ? " Mobile" : ""; 62 | switch (userAgentPlatform as NodeJS.Platform) { 63 | case "darwin": 64 | return `Mozilla/5.0 (Macintosh; Intel Mac OS X ${osVersion.replace(".", "_")}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}${mobileAgent} Safari/537.36` as const; 65 | case "win32": { 66 | const wow64 = (replace?.arch ?? process.arch).endsWith("64") ? "Win64; x64" : "Win32"; 67 | return `Mozilla/5.0 (Windows NT ${osVersion}; ${wow64}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}${mobileAgent} Safari/537.36` as const; 68 | } 69 | case "android": 70 | return `Mozilla/5.0 (Linux; Android ${osVersion}${device}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}${mobileAgent} Safari/537.36` as const; 71 | default: { 72 | const arch = (replace?.arch !== undefined && replace.arch in agentArchMap ? agentArchMap[replace.arch as keyof typeof agentArchMap] : replace?.arch) ?? (process.arch in agentArchMap ? agentArchMap[process.arch as keyof typeof agentArchMap] : process.arch); 73 | return `Mozilla/5.0 (X11; ${toAgentCase(userAgentPlatform)} ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion}${mobileAgent} Safari/537.36` as const; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/modules/arrpc.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../config.ts"; 2 | import { getGoofCordFolderPath } from "../utils.ts"; 3 | import { mainWindow } from "../windows/main/main.ts"; 4 | 5 | let initialised = false; 6 | 7 | export async function initArrpc() { 8 | if (!getConfig("arrpc") || initialised) return; 9 | try { 10 | const { default: Server } = await import("arrpc"); 11 | const Bridge = await import("arrpc/src/bridge.js"); 12 | const server = new Server(getGoofCordFolderPath() + "/detectable.json"); 13 | server.on("activity", (data: object) => Bridge.send(data)); 14 | server.on("invite", (code: string) => { 15 | mainWindow.show(); 16 | Bridge.send({ 17 | cmd: "INVITE_BROWSER", 18 | args: { 19 | "code": code 20 | } 21 | }) 22 | }); 23 | 24 | initialised = true; 25 | } catch (e) { 26 | console.error("Failed to start arRPC server", e); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/assetLoader.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import pc from "picocolors"; 4 | import { getGoofCordFolderPath, readOrCreateFolder } from "../utils.ts"; 5 | import { mainWindow } from "../windows/main/main.ts"; 6 | 7 | export const LOG_PREFIX = pc.yellowBright("[Asset Loader]"); 8 | 9 | export const enabledAssets: Record = { 10 | scripts: [], 11 | styles: [], 12 | }; 13 | 14 | export function getAssets() { 15 | return enabledAssets; 16 | } 17 | 18 | export const assetsFolder = path.join(getGoofCordFolderPath(), "assets/"); 19 | 20 | async function categorizeAssetsByExtension(extension: string) { 21 | try { 22 | const assetFiles = await readOrCreateFolder(assetsFolder); 23 | const categorizedAssets: string[][] = []; 24 | 25 | for (const file of assetFiles) { 26 | const filePath = path.join(assetsFolder, file); 27 | 28 | if (!file.endsWith(extension)) continue; 29 | 30 | const content = await fs.readFile(filePath, "utf-8"); 31 | categorizedAssets.push([file, content]); 32 | } 33 | 34 | console.log(LOG_PREFIX, `Categorized files with extension ${extension}`); 35 | return categorizedAssets; 36 | } catch (err) { 37 | console.error(LOG_PREFIX, `An error occurred while categorizing files with extension ${extension}: ${err}`); 38 | return []; 39 | } 40 | } 41 | 42 | export async function categorizeAllAssets() { 43 | enabledAssets.scripts = await categorizeAssetsByExtension(".js"); 44 | enabledAssets.styles = await categorizeAssetsByExtension(".css"); 45 | } 46 | 47 | export async function startStyleWatcher() { 48 | try { 49 | const watcher = fs.watch(assetsFolder); 50 | 51 | for await (const event of watcher) { 52 | if (event.filename?.endsWith('.css')) { 53 | try { 54 | const filePath = path.join(assetsFolder, event.filename); 55 | const content = await fs.readFile(filePath, "utf-8"); 56 | 57 | mainWindow.webContents.send('assetLoader:styleUpdate', { 58 | file: event.filename, 59 | content 60 | }); 61 | } catch (err) { 62 | if (err instanceof Error && "code" in err && err.code === "ENOENT") { 63 | mainWindow.webContents.send('assetLoader:styleUpdate', { 64 | file: event.filename, 65 | content: "" 66 | }); 67 | } else { 68 | console.error(LOG_PREFIX, `Error processing style update: ${err}`); 69 | } 70 | } 71 | } 72 | } 73 | } catch (err) { 74 | console.error(LOG_PREFIX, `Failed to start style watcher: ${err}`); 75 | } 76 | } -------------------------------------------------------------------------------- /src/modules/cacheManager.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import { app } from "electron"; 4 | import { mainWindow } from "../windows/main/main.ts"; 5 | 6 | export async function clearCache() { 7 | console.log("Clearing cache"); 8 | void mainWindow.webContents.executeJavaScript(`goofcord.titlebar.flashTitlebar("#5865F2")`); 9 | 10 | const userDataPath = app.getPath("userData"); 11 | // Get all folders 12 | const folders = (await fs.readdir(userDataPath, { withFileTypes: true })).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name); 13 | 14 | for (const folder of folders) { 15 | if (folder.toLowerCase().includes("cache")) { 16 | try { 17 | void fs.rm(path.join(userDataPath, folder), { recursive: true, force: true }); 18 | } catch (e) {} 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/dynamicIcon.ts: -------------------------------------------------------------------------------- 1 | import { type NativeImage, app, nativeImage } from "electron"; 2 | import { getTrayIcon, tray } from "../tray.ts"; 3 | import { getAsset } from "../utils.ts"; 4 | import { mainWindow } from "../windows/main/main.ts"; 5 | 6 | const badgeCache = new Map(); 7 | function loadBadge(index: number) { 8 | const clampedIndex = Math.min(index, 10); 9 | 10 | const cached = badgeCache.get(clampedIndex); 11 | if (cached) return cached; 12 | 13 | const img = nativeImage.createFromPath(getAsset(`badges/${clampedIndex}.png`)); 14 | badgeCache.set(clampedIndex, img); 15 | 16 | return img; 17 | } 18 | 19 | export async function setBadgeCount(count: number) { 20 | switch (process.platform) { 21 | case "linux": 22 | app.setBadgeCount(count); 23 | break; 24 | case "darwin": 25 | if (count === 0) { 26 | app.dock?.setBadge(""); 27 | break; 28 | } 29 | app.dock?.setBadge(count.toString()); 30 | break; 31 | case "win32": 32 | mainWindow.setOverlayIcon(loadBadge(count), `${count} Notifications`); 33 | break; 34 | } 35 | if (process.platform === "darwin") return; 36 | tray.setImage(await loadTrayImage(count)); 37 | } 38 | 39 | const trayCache = new Map(); 40 | async function loadTrayImage(index: number) { 41 | const trayImagePath = await getTrayIcon(); 42 | 43 | const clampedIndex = Math.min(index, 10); 44 | 45 | const cached = trayCache.get(clampedIndex); 46 | if (cached) return cached; 47 | 48 | if (clampedIndex === 0) { 49 | const trayImage = nativeImage.createFromPath(trayImagePath); 50 | if (process.platform === "darwin") trayImage.resize({ width: 22, height: 22 }); 51 | trayCache.set(clampedIndex, trayImage); 52 | return trayImage; 53 | } 54 | 55 | const trayImage: string = await mainWindow.webContents.executeJavaScript(` 56 | (async () => { 57 | let data; 58 | 59 | canvas = document.createElement("canvas"); 60 | canvas.width = 128; 61 | canvas.height = 128; 62 | 63 | const img = new Image(); 64 | img.width = 128; 65 | img.height = 128; 66 | 67 | img.onload = () => { 68 | const ctx = canvas.getContext("2d"); 69 | if (ctx) { 70 | ctx.drawImage(img, 0, 0); 71 | 72 | const overlaySize = Math.round(img.width * 0.6); 73 | 74 | const iconImg = new Image(); 75 | iconImg.width = overlaySize; 76 | iconImg.height = overlaySize; 77 | 78 | iconImg.onload = () => { 79 | ctx.drawImage(iconImg, img.width-overlaySize, img.height-overlaySize, overlaySize, overlaySize); 80 | data = canvas.toDataURL(); 81 | }; 82 | 83 | iconImg.src = "${nativeImage.createFromPath(getAsset(`badges/${clampedIndex}.png`)).toDataURL()}"; 84 | } 85 | }; 86 | 87 | img.src = "${nativeImage.createFromPath(trayImagePath).resize({width: 128, height: 128}).toDataURL()}"; 88 | 89 | while (!data) { 90 | await new Promise((resolve) => setTimeout(resolve, 500)); 91 | } 92 | return data; 93 | 94 | })(); 95 | `); 96 | 97 | const nativeTrayImage = nativeImage.createFromDataURL(trayImage); 98 | 99 | if (process.platform === "darwin") nativeTrayImage.resize({ width: 22, height: 22 }); 100 | 101 | trayCache.set(clampedIndex, nativeTrayImage); 102 | 103 | return nativeTrayImage; 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/firewall.ts: -------------------------------------------------------------------------------- 1 | // This file contains everything that uses session.defaultSession.webRequest 2 | import { session } from "electron"; 3 | import pc from "picocolors"; 4 | import { getConfig, getDefaultValue } from "../config.ts"; 5 | import type { ConfigKey, ConfigValue } from "../configTypes.d.ts"; 6 | 7 | function getConfigOrDefault(toGet: K): ConfigValue { 8 | return getConfig("customFirewallRules") ? getConfig(toGet) : getDefaultValue(toGet); 9 | } 10 | 11 | export function initFirewall() { 12 | if (!getConfig("firewall")) return; 13 | const blocklist = getConfigOrDefault("blocklist"); 14 | const blockedStrings = getConfigOrDefault("blockedStrings"); 15 | const allowedStrings = getConfigOrDefault("allowedStrings"); 16 | 17 | // If blocklist is not empty 18 | if (blocklist[0] !== "") { 19 | // Blocking URLs. This list works in tandem with "blockedStrings" list. 20 | session.defaultSession.webRequest.onBeforeRequest( 21 | { 22 | urls: blocklist, 23 | }, 24 | (_, callback) => callback({ cancel: true }), 25 | ); 26 | } 27 | 28 | /* If the request url includes any of those, it is blocked. 29 | * By doing so, we can match multiple unwanted URLs, making the blocklist cleaner and more efficient */ 30 | const blockRegex = new RegExp(blockedStrings.join("|"), "i"); // 'i' flag for case-insensitive matching 31 | const allowRegex = new RegExp(allowedStrings.join("|"), "i"); 32 | 33 | session.defaultSession.webRequest.onBeforeSendHeaders({ urls: [""] }, (details, callback) => { 34 | // No need to filter non-xhr requests 35 | if (details.resourceType !== "xhr") return callback({ cancel: false }); 36 | 37 | if (blockRegex.test(details.url)) { 38 | if (!allowRegex.test(details.url)) { 39 | return callback({ cancel: true }); 40 | } 41 | } 42 | 43 | callback({ cancel: false }); 44 | }); 45 | 46 | console.log(pc.red("[Firewall]"), "Firewall initialized"); 47 | } 48 | 49 | export async function unstrictCSP() { 50 | session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, done) => { 51 | if (!responseHeaders) return done({}); 52 | 53 | //responseHeaders["access-control-allow-origin"] = ["*"]; 54 | if (resourceType === "mainFrame" || resourceType === "subFrame") { 55 | responseHeaders["content-security-policy"] = [""]; 56 | } else if (resourceType === "stylesheet") { 57 | // Fix hosts that don't properly set the css content type, such as raw.githubusercontent.com 58 | responseHeaders["content-type"] = ["text/css"]; 59 | } 60 | done({ responseHeaders }); 61 | }); 62 | console.log(pc.red("[Firewall]"), "Set up CSP unstricter"); 63 | } 64 | -------------------------------------------------------------------------------- /src/modules/localization.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { app } from "electron"; 3 | import { getConfig, setConfig } from "../config.ts"; 4 | import { getAsset } from "../utils.ts"; 5 | 6 | let localization: object; 7 | let defaultLang: object; 8 | 9 | export async function initLocalization() { 10 | let lang = getConfig("locale", true); 11 | if (!lang) { 12 | const possibleLocales = fs.readdirSync(getAsset("lang")).map((file) => file.replace(".json", "")); 13 | await app.whenReady(); 14 | lang = app.getPreferredSystemLanguages()[0]; 15 | if (!possibleLocales.includes(lang)) lang = "en-US"; 16 | void setConfig("locale", lang); 17 | } 18 | localization = JSON.parse(fs.readFileSync(getAsset("lang/" + lang + ".json"), "utf-8")); 19 | defaultLang = JSON.parse(fs.readFileSync(getAsset("lang/en-US.json"), "utf-8")); 20 | } 21 | 22 | // Gets localized string. Shortened because it's used very often 23 | export function i(key: string) { 24 | const translated = localization[key]; 25 | if (translated !== undefined) return translated; 26 | return defaultLang[key]; 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/logger.ts: -------------------------------------------------------------------------------- 1 | // Minimal renderer process logger to make things look prettier 2 | function _log(level: "log" | "error" | "warn" | "info" | "debug", args: string) { 3 | console[level]("%c GoofCord ", "background: #5865f2; color: black; font-weight: bold; border-radius: 5px;", "", args); 4 | } 5 | 6 | export function log(args: string) { 7 | _log("log", args); 8 | } 9 | 10 | export function error(args: string) { 11 | _log("error", args); 12 | } 13 | 14 | export function warn(args: string) { 15 | _log("warn", args); 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/messageEncryption.ts: -------------------------------------------------------------------------------- 1 | import { dialog } from "electron"; 2 | import pc from "picocolors"; 3 | import StegCloak from "stegcloak"; 4 | import { getConfig } from "../config.ts"; 5 | import { decryptSafeStorage, getErrorMessage } from "../utils.ts"; 6 | import { mainWindow } from "../windows/main/main.ts"; 7 | 8 | let stegcloak: StegCloak; 9 | export const encryptionPasswords: string[] = []; 10 | let chosenPassword: string; 11 | let encryptionMark: string; 12 | let cover: string; 13 | 14 | export function initEncryption() { 15 | try { 16 | void loadPasswords(); 17 | void loadCover(); 18 | stegcloak = new StegCloak(true, false); 19 | encryptionMark = getConfig("encryptionMark"); 20 | } catch (error) { 21 | console.error("Encryption initialization error:", error); 22 | } 23 | } 24 | 25 | async function loadPasswords() { 26 | const encryptedPasswords = getConfig("encryptionPasswords"); 27 | try { 28 | for (const password of encryptedPasswords) { 29 | if (!password) continue; 30 | encryptionPasswords.push(decryptSafeStorage(password)); 31 | } 32 | chosenPassword = encryptionPasswords[0]; 33 | console.log(pc.magenta("[Message Encryption]"), "Loaded encryption passwords"); 34 | } catch (error) { 35 | console.error("Failed to load encryption passwords:", error); 36 | } 37 | } 38 | 39 | async function loadCover() { 40 | cover = getConfig("encryptionCover"); 41 | // Stegcloak requires a two-word cover, so we add two invisible characters to the cover if the user wants to use an empty cover or one word cover 42 | if (cover === "" || cover.split(" ").length < 2) { 43 | cover = `${cover}\u200c \u200c`; 44 | } 45 | } 46 | 47 | export function encryptMessage(message: string) { 48 | try { 49 | return stegcloak.hide(`${message}\u200b`, chosenPassword, cover); 50 | } catch (e: unknown) { 51 | console.error("Failed to encrypt message:", e); 52 | dialog.showErrorBox("GoofCord was unable to encrypt your message, did you setup message encryption?", getErrorMessage(e)); 53 | return ""; 54 | } 55 | } 56 | 57 | export function decryptMessage(message: string) { 58 | // A quick way to check if the message was encrypted. 59 | // Character \u200c is present in every stegcloaked message 60 | try { 61 | if (!message.includes("\u200c") || !getConfig("messageEncryption")) return message; 62 | } catch (e) { 63 | return message; 64 | } 65 | 66 | for (const password in encryptionPasswords) { 67 | // If the password is correct, return a decrypted message. Otherwise, try the next password. 68 | try { 69 | const decryptedMessage = stegcloak.reveal(message, encryptionPasswords[password]); 70 | if (decryptedMessage.endsWith("\u200b")) { 71 | return encryptionMark + decryptedMessage; 72 | } 73 | } catch (e) {} 74 | } 75 | return message; 76 | } 77 | 78 | let currentIndex = 0; 79 | export function cycleThroughPasswords() { 80 | currentIndex = (currentIndex + 1) % encryptionPasswords.length; 81 | chosenPassword = encryptionPasswords[currentIndex]; 82 | void mainWindow.webContents.executeJavaScript(`goofcord.titlebar.flashTitlebarWithText("#f9c23c", "Chosen password: ${chosenPassword.slice(0, 2)}...")`); 83 | } 84 | -------------------------------------------------------------------------------- /src/modules/mods.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { Notification } from "electron"; 4 | import pc from "picocolors"; 5 | import { getConfig, setConfig } from "../config.ts"; 6 | import { getErrorMessage, isPathAccessible } from "../utils.ts"; 7 | import { assetsFolder } from "./assetLoader.ts"; 8 | 9 | export const LOG_PREFIX = pc.yellow("[Mod Loader]"); 10 | 11 | const MOD_BUNDLES_URLS: { 12 | [key: string]: [string, string | undefined]; 13 | } = { 14 | vencord: ["https://github.com/Vendicated/Vencord/releases/download/devbuild/browser.js", "https://github.com/Vendicated/Vencord/releases/download/devbuild/browser.css"], 15 | equicord: ["https://github.com/Equicord/Equicord/releases/download/latest/browser.js", "https://github.com/Equicord/Equicord/releases/download/latest/browser.css"], 16 | shelter: ["https://raw.githubusercontent.com/uwu/shelter-builds/main/shelter.js", undefined], 17 | custom: [getConfig("customJsBundle"), getConfig("customCssBundle")], 18 | }; 19 | 20 | async function downloadBundles(urls: Array, name: string) { 21 | const cache = getConfig("modEtagCache"); 22 | let logged = false; 23 | for (const url of urls) { 24 | if (!url) continue; 25 | try { 26 | const filepath = path.join(assetsFolder, `${name}${path.extname(url)}`); 27 | 28 | const exists = await isPathAccessible(filepath); 29 | 30 | const response = await fetch(url, { 31 | headers: { 32 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.3', 33 | "If-None-Match": exists ? cache[url] ?? "" : "", 34 | }, 35 | }); 36 | 37 | // Up to date 38 | if (response.status === 304) continue; 39 | 40 | cache[url] = response.headers.get("ETag"); 41 | 42 | if (!logged) console.log(LOG_PREFIX, "Downloading mod bundles for:", name); 43 | logged = true; 44 | 45 | const bundle = await response.text(); 46 | 47 | await fs.promises.writeFile(filepath, bundle, "utf-8"); 48 | } catch (e: unknown) { 49 | console.error(LOG_PREFIX, `Failed to download ${name} bundle:`, e); 50 | const notification = new Notification({ 51 | title: `Failed to download ${name} mod bundle`, 52 | body: getErrorMessage(e), 53 | timeoutType: "default", 54 | }); 55 | notification.show(); 56 | return; 57 | } 58 | } 59 | if (logged) { 60 | await setConfig("modEtagCache", cache); 61 | console.log(LOG_PREFIX, "Bundles downloaded for:", name); 62 | } 63 | } 64 | 65 | const enabledMods: string[] = []; 66 | export async function manageMods() { 67 | enabledMods.length = 0; 68 | const modNames: string[] = getConfig("modNames"); 69 | const possibleMods = Object.keys(MOD_BUNDLES_URLS); 70 | for (const possibleMod of possibleMods) { 71 | const possibleModPath = path.join(assetsFolder, possibleMod); 72 | if (modNames.includes(possibleMod)) { 73 | enabledMods.push(possibleMod); 74 | try { 75 | await fs.promises.rename(possibleModPath + ".js.disabled", possibleModPath + ".js"); 76 | await fs.promises.rename(possibleModPath + ".css.disabled", possibleModPath + ".css"); 77 | } catch (e) {} 78 | } else { 79 | try { 80 | await fs.promises.rename(possibleModPath + ".js", possibleModPath + ".js.disabled"); 81 | await fs.promises.rename(possibleModPath + ".css", possibleModPath + ".css.disabled"); 82 | } catch (e) {} 83 | } 84 | } 85 | } 86 | 87 | export async function updateMods() { 88 | if (getConfig("noBundleUpdates")) { 89 | console.log(LOG_PREFIX, "Skipping bundle downloads"); 90 | return; 91 | } 92 | for (const mod of enabledMods) { 93 | await downloadBundles(MOD_BUNDLES_URLS[mod], mod); 94 | } 95 | } 96 | 97 | export async function updateModsFull() { 98 | await manageMods(); 99 | await updateMods(); 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/updateCheck.ts: -------------------------------------------------------------------------------- 1 | import { Notification, shell } from "electron"; 2 | import { getConfig } from "../config.ts"; 3 | import { getVersion } from "../utils.ts"; 4 | 5 | async function getLatestVersion(): Promise { 6 | try { 7 | const response = await fetch("https://api.github.com/repos/Milkshiift/GoofCord/releases/latest"); 8 | const data = await response.json(); 9 | return data.tag_name.replace("v", ""); 10 | } catch (e) { 11 | console.error("Failed to fetch the latest GitHub release information. Possibly API rate limit exceeded or timeout reached"); 12 | return getVersion(); 13 | } 14 | } 15 | 16 | export async function checkForUpdate() { 17 | if (!getConfig("updateNotification")) return; 18 | 19 | if (isSemverLower(getVersion(), await getLatestVersion())) { 20 | const notification = new Notification({ 21 | title: "A new update is available ✨", 22 | body: "Click on the notification to download", 23 | timeoutType: "default", 24 | }); 25 | 26 | notification.on("click", () => { 27 | shell.openExternal("https://github.com/Milkshiift/GoofCord/releases/latest"); 28 | }); 29 | 30 | notification.show(); 31 | } 32 | } 33 | 34 | function isSemverLower(version1: string, version2: string): boolean { 35 | function compareParts(a: string | number | undefined, b: string | number | undefined): number { 36 | const aNum = typeof a === 'number' || (typeof a === 'string' && !Number.isNaN(Number(a))); 37 | const bNum = typeof b === 'number' || (typeof b === 'string' && !Number.isNaN(Number(b))); 38 | 39 | if (aNum && bNum) return (Number(a) ?? 0) - (Number(b) ?? 0); 40 | if (aNum) return -1; 41 | if (bNum) return 1; 42 | return String(a ?? '').localeCompare(String(b ?? '')); 43 | } 44 | 45 | function compareVersionParts(v1: string, v2: string): boolean { 46 | const [v1Main, v1Pre] = v1.split('-'); 47 | const [v2Main, v2Pre] = v2.split('-'); 48 | 49 | const v1MainParts = v1Main.split('.').map(Number); 50 | const v2MainParts = v2Main.split('.').map(Number); 51 | 52 | const v1IsPreRelease = v1Pre !== undefined; 53 | const v2IsPreRelease = v2Pre !== undefined; 54 | 55 | if (!v1IsPreRelease && v2IsPreRelease) { 56 | return false; 57 | } 58 | if(v1IsPreRelease && !v2IsPreRelease){ 59 | return true; 60 | } 61 | 62 | for (let i = 0; i < Math.max(v1MainParts.length, v2MainParts.length); i++) { 63 | const cmp = compareParts(v1MainParts[i], v2MainParts[i]); 64 | if (cmp !== 0) return cmp < 0; 65 | } 66 | const v1PreParts = v1Pre ? v1Pre.split('.') : []; 67 | const v2PreParts = v2Pre ? v2Pre.split('.') : []; 68 | 69 | if (!v1Pre && v2Pre) return false; 70 | if (v1Pre && !v2Pre) return true; 71 | 72 | for (let i = 0; i < Math.max(v1PreParts.length, v2PreParts.length); i++) { 73 | const cmp = compareParts(v1PreParts[i], v2PreParts[i]); 74 | if (cmp !== 0) return cmp < 0; 75 | } 76 | 77 | 78 | return false; 79 | } 80 | 81 | return compareVersionParts(version1, version2); 82 | } 83 | -------------------------------------------------------------------------------- /src/modules/venbind.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import type { Venbind as VenbindType } from "venbind"; 3 | import { getAsset } from "../utils.ts"; 4 | import { mainWindow } from "../windows/main/main.ts"; 5 | import pc from "picocolors"; 6 | 7 | let venbind: VenbindType | undefined = undefined; 8 | let venbindLoadAttempted = false; 9 | export async function obtainVenbind() { 10 | if (venbind !== undefined || process.argv.some((arg) => arg === "--no-venbind") || venbindLoadAttempted) return venbind; 11 | try { 12 | venbind = (createRequire(import.meta.url)(getAsset(`venbind-${process.platform}-${process.arch}.node`))); 13 | if (!venbind) throw new Error("Venbind is undefined"); 14 | await startVenbind(venbind); 15 | console.log(pc.green("[Venbind]"), "Loaded venbind"); 16 | } catch (e: unknown) { 17 | console.error("Failed to import venbind", e); 18 | } 19 | venbindLoadAttempted = true; 20 | return venbind; 21 | } 22 | 23 | export async function startVenbind(venbind: VenbindType) { 24 | venbind?.defineErrorHandle((err: string) => { 25 | console.error("venbind error:", err); 26 | }); 27 | venbind?.startKeybinds((id, keyup) => { 28 | if (mainWindow.isFocused()) return; 29 | mainWindow.webContents.send("keybinds:trigger", id, keyup); 30 | }, null); 31 | } 32 | 33 | export async function setKeybinds(keybinds: { id: string; name?: string; shortcut?: string }[]) { 34 | console.log(pc.green("[Venbind]"), "Setting keybinds"); 35 | (await obtainVenbind())?.setKeybinds(keybinds); 36 | } 37 | 38 | export async function isVenbindLoaded() { 39 | return (await obtainVenbind()) !== undefined; 40 | } -------------------------------------------------------------------------------- /src/modules/venmic.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import type { LinkData, PatchBay } from "@vencord/venmic"; 3 | import { app } from "electron"; 4 | import pc from "picocolors"; 5 | import { getAsset } from "../utils.ts"; 6 | 7 | let patchBay: typeof PatchBay; 8 | let venmic: PatchBay; 9 | export let hasPipewirePulse = false; 10 | 11 | export async function initVenmic() { 12 | if (process.argv.some((arg) => arg === "--no-venmic") || patchBay !== undefined) return; 13 | try { 14 | patchBay = (createRequire(import.meta.url)(getAsset(`venmic-${process.arch}.node`)) as typeof import("@vencord/venmic")).PatchBay; 15 | venmic = new patchBay(); 16 | hasPipewirePulse = patchBay.hasPipeWire(); 17 | } catch (e: unknown) { 18 | console.error("Failed to import venmic", e); 19 | } 20 | } 21 | 22 | function getRendererAudioServicePid() { 23 | return app.getAppMetrics().find((proc) => proc.name === "Audio Service")?.pid?.toString() ?? "owo"; 24 | } 25 | 26 | export function venmicList() { 27 | const audioPid = getRendererAudioServicePid(); 28 | return venmic.list(["node.name"]).filter((s) => s["application.process.id"] !== audioPid); 29 | } 30 | 31 | export function venmicStartSystem() { 32 | const pid = getRendererAudioServicePid(); 33 | 34 | // A totally awesome config made by trial and error that hopefully works for most cases. 35 | // only_speakers is disabled because with it enabled Venmic only captured the output of EasyEffects. 36 | // Couldn't get Bitwig Studio captured no matter what I tried :( 37 | const data: LinkData = { 38 | include: [ 39 | { "media.class": "Stream/Output/Audio" }, 40 | ], 41 | exclude: [ 42 | { "application.process.id": pid }, 43 | { "media.class": "Stream/Input/Audio" } 44 | ], 45 | only_speakers: false, 46 | ignore_devices: true, 47 | only_default_speakers: false, 48 | }; 49 | 50 | return venmic.link(data); 51 | } 52 | 53 | export function stopVenmic() { 54 | if (!venmic) return; 55 | console.log(pc.cyan("[Screenshare]"), "Stopping Venmic..."); 56 | venmic.unlink(); 57 | } -------------------------------------------------------------------------------- /src/modules/windowStateManager.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserWindow } from "electron"; 2 | import { getConfig, setConfig } from "../config.ts"; 3 | import type { ConfigKey } from "../configTypes"; 4 | 5 | type NumberPair = [number, number]; 6 | type WindowState = [boolean, NumberPair]; 7 | 8 | export function adjustWindow(window: BrowserWindow, configEntry: string) { 9 | const previousWindowState = getConfig(configEntry as ConfigKey) as WindowState; 10 | const [osMaximized, [width, height]] = previousWindowState; 11 | window.setSize(width, height); 12 | if (osMaximized) window.maximize(); 13 | 14 | const debouncedSaveState = debounce(async () => await saveState(window, configEntry), 1000); 15 | 16 | for (const event of ["resize", "maximize", "unmaximize"]) { 17 | // @ts-ignore 18 | window.on(event, debouncedSaveState); 19 | } 20 | } 21 | 22 | export async function saveState(window: BrowserWindow, configEntry: string) { 23 | const previousWindowState = getConfig(configEntry as ConfigKey) as WindowState; 24 | 25 | const isMaximized = window.isMaximized(); 26 | let size: NumberPair; 27 | if (isMaximized) { 28 | size = previousWindowState[1]; 29 | } else { 30 | size = window.getSize() as NumberPair; 31 | } 32 | 33 | const windowState: [boolean, [number, number]] = [isMaximized, size]; 34 | await setConfig(configEntry as ConfigKey, windowState); 35 | } 36 | 37 | function debounce) => void>(func: T, timeout = 300) { 38 | let timer: Timer; 39 | return (...args: Parameters): void => { 40 | clearTimeout(timer); 41 | timer = setTimeout(() => func(...args), timeout); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/tray.ts: -------------------------------------------------------------------------------- 1 | import { Menu, Tray, app, dialog, nativeImage } from "electron"; 2 | import { getConfig } from "./config.ts"; 3 | import { i } from "./modules/localization.ts"; 4 | import { saveState } from "./modules/windowStateManager.ts"; 5 | import { getAsset, getCustomIcon, getDisplayVersion } from "./utils.ts"; 6 | import { mainWindow } from "./windows/main/main.ts"; 7 | import { createSettingsWindow } from "./windows/settings/main.ts"; 8 | 9 | export let tray: Tray; 10 | export async function createTray() { 11 | const trayImage = nativeImage.createFromPath(await getTrayIcon()); 12 | 13 | const getTrayMenuIcon = () => { 14 | if (process.platform === "win32") return trayImage.resize({ height: 16 }); 15 | if (process.platform === "linux") return trayImage.resize({ height: 24 }); 16 | if (process.platform === "darwin") return trayImage.resize({ height: 18 }); 17 | return trayImage; 18 | }; 19 | 20 | const contextMenu = Menu.buildFromTemplate([ 21 | { 22 | label: `GoofCord ${getDisplayVersion()}`, 23 | icon: getTrayMenuIcon(), 24 | enabled: false, 25 | }, 26 | { 27 | type: "separator", 28 | }, 29 | { 30 | label: i("goofcord-open"), 31 | click: () => { 32 | if (mainWindow) { 33 | mainWindow.show(); 34 | } else { 35 | dialog.showErrorBox("Failed to open the main window", "The main window did not yet initialize. Are you connected to the internet?"); 36 | } 37 | }, 38 | }, 39 | { 40 | label: i("goofcord-settings"), 41 | click: () => { 42 | createSettingsWindow(); 43 | }, 44 | }, 45 | { 46 | type: "separator", 47 | }, 48 | { 49 | label: i("goofcord-restart"), 50 | accelerator: "Shift+CmdOrCtrl+R", 51 | click: async () => { 52 | await saveState(mainWindow, "windowState:main"); 53 | app.relaunch(); 54 | app.exit(); 55 | }, 56 | }, 57 | { 58 | label: i("goofcord-quit"), 59 | click: async () => { 60 | await saveState(mainWindow, "windowState:main"); 61 | app.exit(); 62 | }, 63 | }, 64 | ]); 65 | 66 | await app.whenReady(); 67 | 68 | if (process.platform === "darwin") { 69 | app.dock?.setMenu(contextMenu); 70 | } else { 71 | tray = new Tray(trayImage); 72 | tray.setContextMenu(contextMenu); 73 | tray.setToolTip("GoofCord"); 74 | tray.on("click", () => { 75 | mainWindow.show(); 76 | }); 77 | } 78 | } 79 | 80 | export async function getTrayIcon() { 81 | if (getConfig("trayIcon") === "symbolic_black") return getAsset("gf_symbolic_black.png"); 82 | if (getConfig("trayIcon") === "symbolic_white") return getAsset("gf_symbolic_white.png"); 83 | return getCustomIcon(); 84 | } 85 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { app, safeStorage } from "electron"; 5 | import packageInfo from "../package.json" assert { type: "json" }; 6 | import { getConfig } from "./config.ts"; 7 | 8 | try { 9 | const portablePath = path.join(path.dirname(app.getPath("exe")), "goofcord-data"); 10 | if (await isPathAccessible(portablePath)) { 11 | app.setPath("userData", portablePath); 12 | console.log("Found \"goofcord-data\" folder, running in portable mode"); 13 | } 14 | } catch (e) { 15 | console.error("Portable mode error:", e); 16 | } 17 | 18 | export const dirname = () => path.dirname(fileURLToPath(import.meta.url)); 19 | 20 | export const packageVersion = packageInfo.version; 21 | export const userDataPath = app.getPath("userData"); 22 | 23 | export function getVersion() { 24 | return packageVersion; 25 | } 26 | 27 | export function isDev() { 28 | return process.argv.some((arg) => arg === "--dev" || arg === "-d"); 29 | } 30 | 31 | export function getGoofCordFolderPath() { 32 | return path.join(userDataPath, "/GoofCord/"); 33 | } 34 | 35 | export function getDisplayVersion() { 36 | if (!(app.getVersion() === packageVersion)) { 37 | if (app.getVersion() === process.versions.electron) { 38 | return `Dev Build (${packageVersion})`; 39 | } 40 | return `${packageVersion} [Modified]`; 41 | } 42 | return packageVersion; 43 | } 44 | 45 | export function getAsset(assetName: string) { 46 | return path.join(dirname(), "/assets/", assetName); 47 | } 48 | 49 | export function getCustomIcon() { 50 | const customIconPath = getConfig("customIconPath"); 51 | if (customIconPath !== "") return customIconPath; 52 | if (process.platform === "win32") return getAsset("gf_icon.ico"); 53 | return getAsset("gf_icon.png"); 54 | } 55 | 56 | export async function readOrCreateFolder(path: string) { 57 | try { 58 | return await fs.promises.readdir(path); 59 | } catch (e) { 60 | tryCreateFolder(path); 61 | return []; 62 | } 63 | } 64 | 65 | export function tryCreateFolder(path: string) { 66 | try { 67 | // Synchronous mkdir is literally 100 times faster than the promisified version 68 | fs.mkdirSync(path, { recursive: true }); 69 | } catch (e: unknown) { 70 | if (e instanceof Error && "code" in e && e.code !== "EEXIST") { 71 | console.error(e); 72 | return "EEXIST"; 73 | } 74 | } 75 | return "OK"; 76 | } 77 | 78 | type ErrorWithMessage = { 79 | message: string; 80 | }; 81 | 82 | function isErrorWithMessage(error: unknown): error is ErrorWithMessage { 83 | return typeof error === "object" && error !== null && "message" in error && typeof (error as Record).message === "string"; 84 | } 85 | 86 | function toErrorWithMessage(maybeError: unknown): ErrorWithMessage { 87 | if (isErrorWithMessage(maybeError)) { 88 | return maybeError; 89 | } 90 | 91 | try { 92 | return new Error(JSON.stringify(maybeError)); 93 | } catch { 94 | // fallback in case there's an error stringifying the maybeError 95 | // with circular references for example. 96 | return new Error(String(maybeError)); 97 | } 98 | } 99 | 100 | export function getErrorMessage(error: unknown): string { 101 | return toErrorWithMessage(error).message; 102 | } 103 | 104 | export function isEncryptionAvailable() { 105 | return safeStorage.isEncryptionAvailable(); 106 | } 107 | 108 | export function encryptSafeStorage(plaintextString: string) { 109 | return isEncryptionAvailable() ? safeStorage.encryptString(plaintextString).toString("base64") : plaintextString; 110 | } 111 | 112 | export function decryptSafeStorage(encryptedBase64: string) { 113 | return isEncryptionAvailable() ? safeStorage.decryptString(Buffer.from(encryptedBase64, "base64")) : encryptedBase64; 114 | } 115 | 116 | export async function saveFileToGCFolder(filePath: string, content: string) { 117 | const fullPath = path.join(getGoofCordFolderPath(), filePath); 118 | await fs.promises.writeFile(fullPath, content); 119 | return fullPath; 120 | } 121 | 122 | export async function isPathAccessible(filePath: string) { 123 | try { 124 | await fs.promises.access(filePath, fs.constants.F_OK); 125 | return true; 126 | } catch (error) { 127 | return false; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/windows/main/bridge.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron"; 2 | import type { ConfigKey } from "../../configTypes"; 3 | import { flashTitlebar, flashTitlebarWithText } from "./titlebar.ts"; 4 | 5 | contextBridge.exposeInMainWorld("goofcord", { 6 | window: { 7 | show: () => ipcRenderer.invoke("window:Show"), 8 | hide: () => ipcRenderer.invoke("window:Hide"), 9 | minimize: () => ipcRenderer.invoke("window:Minimize"), 10 | maximize: () => ipcRenderer.invoke("window:Maximize"), 11 | }, 12 | titlebar: { 13 | flashTitlebar: (color: string) => flashTitlebar(color), 14 | flashTitlebarWithText: (color: string, text: string) => flashTitlebarWithText(color, text), 15 | }, 16 | electron: process.versions.electron, 17 | version: ipcRenderer.sendSync("utils:getVersion"), 18 | displayVersion: ipcRenderer.sendSync("utils:getDisplayVersion"), 19 | loadConfig: () => ipcRenderer.invoke("config:loadConfig"), 20 | getConfig: (toGet: string, bypassDefault = false) => ipcRenderer.sendSync("config:getConfig", toGet, bypassDefault), 21 | setConfig: (key: string, value: ConfigKey) => ipcRenderer.invoke("config:setConfig", key, value), 22 | encryptMessage: (message: string) => ipcRenderer.sendSync("messageEncryption:encryptMessage", message), 23 | decryptMessage: (message: string) => ipcRenderer.sendSync("messageEncryption:decryptMessage", message), 24 | cycleThroughPasswords: () => ipcRenderer.invoke("messageEncryption:cycleThroughPasswords"), 25 | openSettingsWindow: () => ipcRenderer.invoke("main:createSettingsWindow"), 26 | setBadgeCount: (count: number) => ipcRenderer.invoke("dynamicIcon:setBadgeCount", count), 27 | stopVenmic: () => ipcRenderer.invoke("venmic:stopVenmic"), 28 | }); 29 | -------------------------------------------------------------------------------- /src/windows/main/defaultAssets.ts: -------------------------------------------------------------------------------- 1 | import type { Patch } from "@vencord/types/utils/types"; 2 | import { ipcRenderer } from "electron"; 3 | 4 | // This whole file is messed up 5 | 6 | let patchesScript = ` 7 | (() => { 8 | if (!window.Vencord.Plugins.patches) return; 9 | window.GCDP = {}; 10 | `; 11 | 12 | interface PatchData { 13 | patches: Omit[]; 14 | // biome-ignore lint/suspicious/noExplicitAny: 15 | [key: string]: any; 16 | } 17 | 18 | export function addPatch(p: PatchData) { 19 | const { patches, ...globals } = p; 20 | 21 | patches.map((patch)=>{ 22 | if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement]; 23 | for (const r of patch.replacement) { 24 | if (typeof r.replace === "string") r.replace = r.replace.replaceAll("$self", "GCDP"); 25 | if (typeof r.match !== "string") { // @ts-ignore 26 | r.match = [r.match.source, r.match.flags]; 27 | } 28 | } 29 | // @ts-ignore 30 | patch.plugin = "GoofCord"; 31 | return patch; 32 | }) 33 | 34 | patchesScript += ` 35 | window.Vencord.Plugins.patches.push(...${JSON.stringify(patches)}.map((patch)=>{ 36 | for (const r of patch.replacement) { 37 | if (Array.isArray(r.match)) { 38 | r.match = new RegExp(r.match[0], r.match[1]); 39 | } 40 | } 41 | return patch; 42 | })); 43 | `; 44 | 45 | for (const globalF in globals) { 46 | patchesScript += `\nwindow.GCDP.${globalF}=function ${String(globals[globalF])}`; 47 | } 48 | } 49 | 50 | export const scripts: string[][] = []; 51 | 52 | export function getDefaultScripts() { 53 | scripts.push(["notificationFix", ` 54 | (() => { 55 | const originalSetter = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick").set; 56 | Object.defineProperty(Notification.prototype, "onclick", { 57 | set(onClick) { 58 | originalSetter.call(this, function() { 59 | onClick.apply(this, arguments); 60 | goofcord.window.show(); 61 | }) 62 | }, 63 | configurable: true 64 | }); 65 | })(); 66 | `]); 67 | 68 | scripts.unshift(["vencordPatches", patchesScript+"})();"]); 69 | 70 | if (ipcRenderer.sendSync("config:getConfig", "domOptimizer")) { 71 | scripts.push(["domOptimizer", ` 72 | function optimize(orig) { 73 | const delayedClasses = ["activity", "gif", "avatar", "imagePlaceholder", "reaction", "hoverBar"]; 74 | 75 | return function (...args) { 76 | const element = args[0]; 77 | //console.log(element); 78 | 79 | if (typeof element?.className === "string") { 80 | if (delayedClasses.some(partial => element.className.includes(partial))) { 81 | //console.log("DELAYED", element.className); 82 | setTimeout(() => orig.apply(this, args), 100 - Math.random() * 50); 83 | return; 84 | } 85 | } 86 | return orig.apply(this, args); 87 | }; 88 | } 89 | Element.prototype.removeChild = optimize(Element.prototype.removeChild); 90 | `]); 91 | } 92 | 93 | if (ipcRenderer.sendSync("config:getConfig", "modNames").includes("shelter") && ipcRenderer.sendSync("config:getConfig", "installDefaultShelterPlugins")) { 94 | scripts.push(["shelterPluginInit", ` 95 | (async()=>{ 96 | while(!window.shelter?.plugins?.addRemotePlugin) await new Promise(resolve => setTimeout(resolve, 1000)); 97 | const defaultPlugins = [ 98 | ["https://spikehd.github.io/shelter-plugins/plugin-browser/", false], 99 | ["https://spikehd.github.io/shelter-plugins/shelteRPC/", true], 100 | ["https://milkshiift.github.io/goofcord-shelter-plugins/dynamic-icon/", true], 101 | ["https://milkshiift.github.io/goofcord-shelter-plugins/console-suppressor/", false], 102 | ["https://milkshiift.github.io/goofcord-shelter-plugins/message-encryption/", true], 103 | ["https://milkshiift.github.io/goofcord-shelter-plugins/invidious-embeds/", true], 104 | ["https://milkshiift.github.io/goofcord-shelter-plugins/settings-category/", true], 105 | ["https://milkshiift.github.io/goofcord-shelter-plugins/webpack-magic/", true], 106 | ]; 107 | for (const plugin of defaultPlugins) { 108 | try { 109 | await window.shelter.plugins.addRemotePlugin(getId(plugin[0]), plugin[0]); 110 | if (plugin[1]) await window.shelter.plugins.startPlugin(getId(plugin[0])); 111 | } catch (e) {} 112 | } 113 | console.log("Added default Shelter plugins"); 114 | function getId(url) { 115 | return url.replace("https://", ""); 116 | } 117 | })() 118 | `]); 119 | } 120 | 121 | return scripts; 122 | } -------------------------------------------------------------------------------- /src/windows/main/keybinds.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer, webFrame } from "electron"; 2 | import { warn } from "../../modules/logger.ts"; 3 | import { addPatch } from "./defaultAssets.ts"; 4 | 5 | addPatch({ 6 | patches: [ 7 | { 8 | find: "keybindActionTypes", 9 | replacement: [ 10 | { 11 | match: /(?:[A-Za-z_$][\w$]*)\.isPlatformEmbedded/g, 12 | replace: "true" 13 | }, 14 | { 15 | match: /\(0,(?:[A-Za-z_$][\w$]*)\.isDesktop\)\(\)/g, 16 | replace: "true" 17 | }, 18 | ] 19 | }, 20 | ] 21 | }); 22 | 23 | interface Keybind { 24 | shortcut: string, 25 | eventSettings: { 26 | keyCode: number, 27 | ctrlKey: boolean, 28 | altKey: boolean, 29 | shiftKey: boolean 30 | } 31 | } 32 | 33 | const getActiveKeybinds = (): Map => { 34 | const activeKeybinds = new Map(); 35 | const keybindsRaw = window.localStorage.getItem("keybinds"); 36 | 37 | if (!keybindsRaw) return activeKeybinds; 38 | 39 | const keybinds = JSON.parse(keybindsRaw)?._state; 40 | if (!keybinds) return activeKeybinds; 41 | 42 | const MODIFIERS = { 43 | CTRL: 17, 44 | ALT: 18, 45 | SHIFT: 16 46 | }; 47 | 48 | for (const bind in keybinds) { 49 | const binding = keybinds[bind]; 50 | 51 | // We are only interested in user defined keybinds 52 | if (binding.managed === true || binding.enabled === false) continue; 53 | 54 | const keys = binding.shortcut.map((x: number[]) => x[1]); 55 | const modifiers = { 56 | ctrl: keys.includes(MODIFIERS.CTRL), 57 | alt: keys.includes(MODIFIERS.ALT), 58 | shift: keys.includes(MODIFIERS.SHIFT) 59 | }; 60 | 61 | // Filter out modifier keys 62 | const mainKeys = keys.filter(key => 63 | ![MODIFIERS.CTRL, MODIFIERS.ALT, MODIFIERS.SHIFT].includes(key) 64 | ); 65 | 66 | // Build keyboard shortcut string 67 | const keyParts: string[] = []; 68 | if (modifiers.ctrl) keyParts.push('ctrl'); 69 | if (modifiers.alt) keyParts.push('alt'); 70 | if (modifiers.shift) keyParts.push('shift'); 71 | 72 | const mainKey = mainKeys.length > 0 ? String.fromCharCode(mainKeys.at(-1)) : ''; 73 | keyParts.push(mainKey); 74 | 75 | if (!mainKey) continue; 76 | 77 | activeKeybinds.set(macroCaseToTitleCase(binding.action), { 78 | shortcut: keyParts.join('+').toLowerCase(), 79 | eventSettings: { 80 | keyCode: mainKeys.at(-1), 81 | ctrlKey: modifiers.ctrl, 82 | altKey: modifiers.alt, 83 | shiftKey: modifiers.shift 84 | } 85 | }); 86 | } 87 | 88 | return activeKeybinds; 89 | }; 90 | 91 | // HELLO_WORLD -> Hello World 92 | const macroCaseToTitleCase = (input: string): string => { 93 | return input.toLowerCase() 94 | .split('_') 95 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 96 | .join(' '); 97 | }; 98 | 99 | let activeKeybinds: Map = getActiveKeybinds(); 100 | 101 | function updateKeybinds() { 102 | activeKeybinds = getActiveKeybinds(); 103 | const toSend: object[] = []; 104 | 105 | for (const [key, value] of activeKeybinds) { 106 | toSend.push({ 107 | id: key, 108 | name: key, 109 | shortcut: value.shortcut 110 | }); 111 | } 112 | console.log(toSend); 113 | void ipcRenderer.invoke("venbind:setKeybinds", toSend); 114 | } 115 | 116 | export function startKeybindWatcher() { 117 | updateKeybinds(); 118 | 119 | contextBridge.exposeInMainWorld("keybinds", { 120 | updateKeybinds: debounce(updateKeybinds,1000) 121 | }) 122 | 123 | void webFrame.executeJavaScript(` 124 | setTimeout(() => { 125 | window.shelter.flux.dispatcher.subscribe("KEYBINDS_SET_KEYBIND", ({keybind}) => { 126 | window.keybinds.updateKeybinds(); 127 | }) 128 | }, 5000); // Time for shelter flux to initialize 129 | `); 130 | } 131 | 132 | ipcRenderer.on('keybinds:getAll', () => { 133 | return activeKeybinds; 134 | }) 135 | 136 | ipcRenderer.on('keybinds:trigger', (_, id, keyup) => { 137 | const keybind = activeKeybinds.get(id); 138 | if (!keybind) { 139 | warn("Keybind not found: "+id); 140 | return; 141 | } 142 | 143 | const event = new KeyboardEvent( 144 | keyup ? 'keyup' : 'keydown', 145 | keybind.eventSettings 146 | ); 147 | 148 | document.dispatchEvent(event); 149 | }); 150 | 151 | function debounce) => void>(func: T, timeout = 300) { 152 | let timer: Timer; 153 | return (...args: Parameters): void => { 154 | clearTimeout(timer); 155 | timer = setTimeout(() => func(...args), timeout); 156 | }; 157 | } -------------------------------------------------------------------------------- /src/windows/main/main.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import * as path from "node:path"; 3 | import { app, BrowserWindow, ipcMain, shell } from "electron"; 4 | import pc from "picocolors"; 5 | import { getConfig } from "../../config.ts"; 6 | import { getUserAgent } from "../../modules/agent.ts"; 7 | import { adjustWindow } from "../../modules/windowStateManager.ts"; 8 | import { dirname, getAsset, getCustomIcon } from "../../utils.ts"; 9 | import { registerScreenshareHandler } from "../screenshare/main.ts"; 10 | 11 | export let mainWindow: BrowserWindow; 12 | 13 | export async function createMainWindow() { 14 | if (process.argv.some((arg) => arg === "--headless")) return; 15 | 16 | console.log(`${pc.blue("[Window]")} Opening window...`); 17 | const transparency: boolean = getConfig("transparency"); 18 | mainWindow = new BrowserWindow({ 19 | title: "GoofCord", 20 | show: true, 21 | darkTheme: true, 22 | icon: getCustomIcon(), 23 | frame: !getConfig("customTitlebar"), 24 | autoHideMenuBar: true, 25 | backgroundColor: transparency ? "#00000000" : "#313338", 26 | transparent: transparency, 27 | backgroundMaterial: transparency ? "acrylic" : "none", 28 | webPreferences: { 29 | sandbox: false, 30 | preload: path.join(dirname(), "windows/main/preload.mjs"), 31 | nodeIntegrationInSubFrames: false, 32 | enableWebSQL: false, 33 | spellcheck: getConfig("spellcheck"), 34 | enableBlinkFeatures: getConfig("autoscroll") ? "MiddleClickAutoscroll" : undefined, 35 | }, 36 | }); 37 | 38 | adjustWindow(mainWindow, "windowState:main"); 39 | if (getConfig("startMinimized")) mainWindow.hide(); 40 | await doAfterDefiningTheWindow(); 41 | } 42 | 43 | async function doAfterDefiningTheWindow() { 44 | console.log(`${pc.blue("[Window]")} Setting up window...`); 45 | 46 | // Set the user agent for the web contents based on the Chrome version. 47 | mainWindow.webContents.userAgent = getUserAgent(process.versions.chrome); 48 | 49 | void mainWindow.loadURL(getConfig("discordUrl")); 50 | 51 | mainWindow.on("close", (event) => { 52 | if (getConfig("minimizeToTray") || process.platform === "darwin") { 53 | event.preventDefault(); 54 | mainWindow.hide(); 55 | } 56 | }); 57 | subscribeToAppEvents(); 58 | setWindowOpenHandler(); 59 | registerScreenshareHandler(); 60 | void initYoutubeAdblocker(); 61 | } 62 | 63 | let subscribed = false; 64 | function subscribeToAppEvents() { 65 | if (subscribed) return; 66 | subscribed = true; 67 | app.on("second-instance", (_event, cmdLine, _cwd, _data) => { 68 | const keybind = cmdLine.find(x => x.startsWith("--keybind")); 69 | if (keybind !== undefined) { 70 | const action = keybind.split("=")[1]; 71 | const keyup: boolean = keybind.startsWith("--keybind-up=") || keybind.startsWith("--keybind="); 72 | if (action !== undefined) { 73 | mainWindow.webContents.send("keybinds:trigger", action, keyup); 74 | } 75 | } else { 76 | mainWindow.restore(); 77 | mainWindow.show(); 78 | } 79 | }); 80 | app.on("activate", () => { 81 | mainWindow.show(); 82 | }); 83 | ipcMain.handle("window:Maximize", () => mainWindow.maximize()); 84 | ipcMain.handle("window:IsMaximized", () => mainWindow.isMaximized()); 85 | ipcMain.handle("window:Minimize", () => mainWindow.minimize()); 86 | ipcMain.handle("window:Unmaximize", () => mainWindow.unmaximize()); 87 | ipcMain.handle("window:Show", () => mainWindow.show()); 88 | ipcMain.handle("window:Hide", () => mainWindow.hide()); 89 | ipcMain.handle("window:Quit", () => mainWindow.close()); 90 | ipcMain.handle("flashTitlebar", (_event, color: string) => { 91 | void mainWindow.webContents.executeJavaScript(`goofcord.titlebar.flashTitlebar("${color}")`); 92 | }); 93 | ipcMain.handle("flashTitlebarWithText", (_event, color: string, text: string) => { 94 | void mainWindow.webContents.executeJavaScript(`goofcord.titlebar.flashTitlebarWithText("${color}", "${text}")`); 95 | }); 96 | } 97 | 98 | function setWindowOpenHandler() { 99 | // Define a handler for opening new windows. 100 | mainWindow.webContents.setWindowOpenHandler(({ url }) => { 101 | if (url === "about:blank") return { action: "allow" }; // For Vencord's quick css 102 | if (url.includes("discord.com/popout")) { 103 | // Allow Discord voice chat popout 104 | return { 105 | action: "allow", 106 | overrideBrowserWindowOptions: { 107 | frame: true, 108 | autoHideMenuBar: true, 109 | icon: getCustomIcon(), 110 | backgroundColor: "#313338", 111 | alwaysOnTop: getConfig("popoutWindowAlwaysOnTop"), 112 | webPreferences: { 113 | sandbox: true, 114 | }, 115 | }, 116 | }; 117 | } 118 | if (["http", "mailto:", "spotify:", "steam:", "com.epicgames.launcher:", "tidal:", "itunes:"].some((prefix) => url.startsWith(prefix))) void shell.openExternal(url); 119 | return { action: "deny" }; 120 | }); 121 | } 122 | 123 | async function initYoutubeAdblocker() { 124 | const adblocker = await fs.readFile(getAsset("adblocker.js"), "utf8"); 125 | mainWindow.webContents.on("frame-created", (_, { frame }) => { 126 | if (!frame) return; 127 | frame.once("dom-ready", () => { 128 | if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) { 129 | frame.executeJavaScript(adblocker); 130 | } 131 | }); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /src/windows/main/preload.mts: -------------------------------------------------------------------------------- 1 | import "./bridge.ts"; 2 | import fs from "node:fs"; 3 | import { ipcRenderer, webFrame } from "electron"; 4 | import { error, log } from "../../modules/logger.ts"; 5 | import { getDefaultScripts } from "./defaultAssets.ts"; 6 | import { injectTitlebar } from "./titlebar.ts"; 7 | import "./screenshare.ts"; 8 | import { startKeybindWatcher, } from "./keybinds.ts"; 9 | 10 | if (ipcRenderer.sendSync("config:getConfig", "disableAltMenu")) { 11 | // https://github.com/electron/electron/issues/34211 12 | window.addEventListener('keydown', (e) => { 13 | if (e.code === 'AltLeft') e.preventDefault(); 14 | }); 15 | } 16 | 17 | const loadedStyles = new Map(); 18 | 19 | if (document.location.hostname.includes("discord")) { 20 | void injectTitlebar(); 21 | 22 | const assets: Record = ipcRenderer.sendSync("assetLoader:getAssets"); 23 | assets.scripts.push(...getDefaultScripts()); 24 | for (const script of assets.scripts) { 25 | webFrame.executeJavaScript(script[1]).then(() => log(`Loaded script: ${script[0]}`)); 26 | } 27 | 28 | startKeybindWatcher(); 29 | 30 | document.addEventListener("DOMContentLoaded", () => { 31 | assets.styles.push(["discord.css", fs.readFileSync(ipcRenderer.sendSync("utils:getAsset", "css/discord.css"), "utf8")]); 32 | if (ipcRenderer.sendSync("config:getConfig", "renderingOptimizations")) { 33 | assets.styles.push(["renderingOptimizations", ` 34 | [class*="messagesWrapper"], #channels, #emoji-picker-grid, [class*="members_"] { 35 | will-change: transform, scroll-position; 36 | contain: strict; 37 | } 38 | `]); 39 | } 40 | for (const style of assets.styles) { 41 | updateStyle(style[1], style[0]); 42 | log(`Loaded style: ${style[0]}`); 43 | } 44 | 45 | ipcRenderer.on('assetLoader:styleUpdate', (_, data) => { 46 | const { file, content } = data; 47 | updateStyle(content, file); 48 | log(`Hot reloaded style: ${file}`); 49 | }); 50 | }); 51 | } 52 | 53 | function updateStyle(style: string, id: string) { 54 | try { 55 | const oldStyleElement = loadedStyles.get(id); 56 | oldStyleElement?.remove(); 57 | } catch (err) { 58 | error(`Error removing old style: ${id} - ${err}`); 59 | } 60 | 61 | const styleElement = document.createElement('style'); 62 | styleElement.textContent = style; 63 | styleElement.id = id; 64 | document.body.appendChild(styleElement); 65 | loadedStyles.set(id, styleElement); 66 | } 67 | -------------------------------------------------------------------------------- /src/windows/main/screenshare.ts: -------------------------------------------------------------------------------- 1 | import { addPatch, scripts } from "./defaultAssets.ts"; 2 | 3 | addPatch({ 4 | patches: [ 5 | { 6 | find: "this.localWant=", 7 | replacement: { 8 | match: /this.localWant=/, 9 | replace: "$self.patchStreamQuality(this);$&" 10 | } 11 | } 12 | ], 13 | // biome-ignore lint/suspicious/noExplicitAny: 14 | patchStreamQuality(opts: any) { 15 | // @ts-ignore 16 | const screenshareQuality = window.screenshareSettings; 17 | if (!screenshareQuality) return; 18 | 19 | const framerate = Number(screenshareQuality.framerate); 20 | const height = Number(screenshareQuality.resolution); 21 | const width = Math.round(height * (screen.width / screen.height)); 22 | 23 | Object.assign(opts, { 24 | bitrateMin: 500000, 25 | bitrateMax: 8000000, 26 | bitrateTarget: 600000 27 | }); 28 | if (opts?.encode) { 29 | Object.assign(opts.encode, { 30 | framerate, 31 | width, 32 | height, 33 | pixelCount: height * width 34 | }); 35 | } 36 | Object.assign(opts.capture, { 37 | framerate, 38 | width, 39 | height, 40 | pixelCount: height * width 41 | }); 42 | } 43 | }); 44 | 45 | scripts.push(["screensharePatch", ` 46 | (() => { 47 | const original = navigator.mediaDevices.getDisplayMedia; 48 | 49 | async function getVirtmic() { 50 | try { 51 | const devices = await navigator.mediaDevices.enumerateDevices(); 52 | const audioDevice = devices.find(({ label }) => label === "vencord-screen-share"); 53 | return audioDevice?.deviceId; 54 | } catch (error) { 55 | return null; 56 | } 57 | } 58 | 59 | navigator.mediaDevices.getDisplayMedia = async function (opts) { 60 | const stream = await original.call(this, opts); 61 | console.log("Setting stream's content hint and audio device"); 62 | 63 | const settings = window.screenshareSettings; 64 | settings.width = Math.round(settings.resolution * (screen.width / screen.height)); 65 | 66 | const videoTrack = stream.getVideoTracks()[0]; 67 | videoTrack.contentHint = settings.contentHint || "motion"; 68 | 69 | const constraints = { 70 | ...videoTrack.getConstraints(), 71 | frameRate: { min: settings.framerate, ideal: settings.framerate }, 72 | width: { min: 640, ideal: settings.width, max: settings.width }, 73 | height: { min: 480, ideal: settings.resolution, max: settings.resolution }, 74 | advanced: [{ width: settings.width, height: settings.resolution }], 75 | resizeMode: "none" 76 | }; 77 | 78 | videoTrack.applyConstraints(constraints).then(() => { 79 | console.log("Applied constraints successfully. New constraints: ", videoTrack.getConstraints()); 80 | }).catch(e => console.error("Failed to apply constraints.", e)); 81 | 82 | // Default audio sharing 83 | const audioTrack = stream.getAudioTracks()[0]; 84 | if (audioTrack) audioTrack.contentHint = "music"; 85 | 86 | // Venmic 87 | const id = await getVirtmic(); 88 | if (id) { 89 | const audio = await navigator.mediaDevices.getUserMedia({ 90 | audio: { 91 | deviceId: { 92 | exact: id 93 | }, 94 | autoGainControl: false, 95 | echoCancellation: false, 96 | noiseSuppression: false, 97 | channelCount: 2, 98 | sampleRate: 48000, 99 | sampleSize: 16 100 | } 101 | }); 102 | 103 | stream.getAudioTracks().forEach(t => stream.removeTrack(t)); 104 | stream.addTrack(audio.getAudioTracks()[0]); 105 | } 106 | 107 | return stream; 108 | }; 109 | 110 | setTimeout(() => { 111 | window.shelter.flux.dispatcher.subscribe("STREAM_CLOSE", ({streamKey}) => { 112 | const owner = streamKey.split(":").at(-1); 113 | if (owner === shelter.flux.stores.UserStore.getCurrentUser().id) { 114 | goofcord.stopVenmic(); 115 | } 116 | }) 117 | }, 5000); // Time for shelter flux to initialize 118 | })(); 119 | `]); -------------------------------------------------------------------------------- /src/windows/main/titlebar.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { ipcRenderer, webFrame } from "electron"; 3 | 4 | interface TitlebarElements { 5 | titlebar: HTMLElement | null; 6 | controls: HTMLElement | null; 7 | dragBar: HTMLElement | null; 8 | titlebarText: HTMLElement | null; 9 | } 10 | 11 | const elements: TitlebarElements = { 12 | titlebar: null, 13 | controls: null, 14 | dragBar: null, 15 | titlebarText: null 16 | }; 17 | 18 | const customTitlebarEnabled = ipcRenderer.sendSync("config:getConfig", "customTitlebar"); 19 | 20 | async function attachControlsEvents(container: Element): Promise { 21 | const minimize = container.querySelector("#minimize"); 22 | const maximize = container.querySelector("#maximize"); 23 | const quit = container.querySelector("#quit"); 24 | 25 | minimize?.addEventListener("click", () => { 26 | void ipcRenderer.invoke("window:Minimize"); 27 | }); 28 | 29 | const isMaximized = await ipcRenderer.invoke("window:IsMaximized"); 30 | if (maximize && isMaximized) maximize.id = "maximized"; 31 | 32 | maximize?.addEventListener("click", async () => { 33 | const isMaximized = await ipcRenderer.invoke("window:IsMaximized"); 34 | if (isMaximized) { 35 | void ipcRenderer.invoke("window:Unmaximize"); 36 | maximize.id = "maximize"; 37 | } else { 38 | void ipcRenderer.invoke("window:Maximize"); 39 | maximize.id = "maximized"; 40 | } 41 | }); 42 | 43 | quit?.addEventListener("click", () => { 44 | void ipcRenderer.invoke("window:Quit"); 45 | }); 46 | } 47 | 48 | function addCustomTitlebar(): void { 49 | elements.titlebarText = document.createElement("p"); 50 | elements.titlebarText.id = "titlebar-text"; 51 | document.body.prepend(elements.titlebarText); 52 | 53 | elements.dragBar = document.createElement('div'); 54 | elements.dragBar.id = "dragbar"; 55 | 56 | if (customTitlebarEnabled) { 57 | elements.dragBar.style.setProperty('-webkit-app-region', 'drag'); 58 | 59 | elements.controls = document.createElement('div'); 60 | elements.controls.id = "window-controls-container"; 61 | elements.controls.innerHTML = '
'; 62 | document.body.prepend(elements.controls); 63 | void attachControlsEvents(elements.controls); 64 | } 65 | 66 | document.body.prepend(elements.dragBar); 67 | } 68 | 69 | function modifyDiscordBar(): void { 70 | if (!customTitlebarEnabled) return; 71 | 72 | const bar = document.querySelector('[data-windows]'); 73 | if (!bar) return; 74 | elements.titlebar = bar as HTMLElement; 75 | 76 | // trigger CSS rules that show custom titlebar 77 | bar.setAttribute("__goofcord-custom-titlebar", "true"); 78 | } 79 | 80 | export async function injectTitlebar(): Promise { 81 | document.addEventListener("DOMContentLoaded", async () => { 82 | addCustomTitlebar(); 83 | 84 | const observer = new MutationObserver(checkMainLayer); 85 | observer.observe(document.body, {childList: true, subtree: true}); 86 | 87 | // Initial check 88 | checkMainLayer(); 89 | 90 | try { 91 | const cssPath = ipcRenderer.sendSync("utils:getAsset", "css/titlebar.css"); 92 | const cssContent = await fs.readFile(cssPath, "utf8"); 93 | webFrame.insertCSS(cssContent); 94 | } catch (error) { 95 | console.error('Failed to load titlebar CSS:', error); 96 | } 97 | }); 98 | } 99 | 100 | function checkMainLayer(): void { 101 | if (!elements.dragBar) return; 102 | 103 | // mainLayer is a parent of the Discord top bar. If it's hidden, show the drag bar as a fallback 104 | const mainLayer = document.querySelector('[aria-hidden][class^="layer_"]'); 105 | 106 | if (!mainLayer) { 107 | elements.dragBar.style.display = "block"; 108 | } else { 109 | elements.dragBar.style.display = mainLayer.getAttribute('aria-hidden') === "true" ? "block" : "none"; 110 | 111 | // `elements.titlebar` may be uninitialized or may have been deleted from the document 112 | // (due to switching accounts etc.) so we check it 113 | if (!mainLayer.contains(elements.titlebar)) modifyDiscordBar(); 114 | } 115 | } 116 | 117 | let animationInProgress = false; 118 | let titlebarTimeout: NodeJS.Timeout; 119 | let originalTitlebarColor = ""; 120 | let originalDragbarColor = ""; 121 | 122 | export function flashTitlebar(color: string): void { 123 | if (!elements.titlebar || !elements.dragBar) return; 124 | 125 | originalTitlebarColor = originalTitlebarColor ?? elements.titlebar.style.backgroundColor; 126 | originalDragbarColor = originalDragbarColor ?? elements.dragBar.style.backgroundColor; 127 | 128 | // Cancel any ongoing animation 129 | if (animationInProgress) { 130 | resetTitlebarColors(originalTitlebarColor, originalDragbarColor); 131 | } 132 | 133 | animationInProgress = true; 134 | 135 | elements.titlebar.style.backgroundColor = color; 136 | elements.dragBar.style.backgroundColor = color; 137 | 138 | const handleTransitionEnd = () => { 139 | resetTitlebarColors(originalTitlebarColor, originalDragbarColor); 140 | }; 141 | 142 | elements.titlebar.addEventListener("transitionend", handleTransitionEnd, { once: true }); 143 | elements.dragBar.addEventListener("transitionend", handleTransitionEnd, { once: true }); 144 | } 145 | 146 | function resetTitlebarColors(titlebarColor: string, dragbarColor: string): void { 147 | if (!elements.titlebar || !elements.dragBar) return; 148 | 149 | elements.titlebar.style.backgroundColor = titlebarColor; 150 | elements.dragBar.style.backgroundColor = dragbarColor; 151 | animationInProgress = false; 152 | } 153 | 154 | export function flashTitlebarWithText(color: string, text: string): void { 155 | flashTitlebar(color); 156 | 157 | if (!elements.titlebarText) return; 158 | 159 | elements.titlebarText.innerHTML = text; 160 | elements.titlebarText.style.transition = "opacity 0.2s ease-out"; 161 | elements.titlebarText.style.opacity = "1"; 162 | 163 | // Clear the previous timeout if it exists 164 | if (titlebarTimeout) { 165 | clearTimeout(titlebarTimeout); 166 | } 167 | 168 | // Hide the text after a delay 169 | titlebarTimeout = setTimeout(() => { 170 | if (elements.titlebarText) { 171 | elements.titlebarText.style.transition = "opacity 2s ease-out"; 172 | elements.titlebarText.style.opacity = "0"; 173 | } 174 | }, 4000); 175 | } -------------------------------------------------------------------------------- /src/windows/preloadUtils.ts: -------------------------------------------------------------------------------- 1 | // Utilities specific to preload scripts 2 | 3 | export function findKeyAtDepth(obj: object, targetKey: string, depth: number) { 4 | if (depth === 1) { 5 | return obj[targetKey] || undefined; 6 | } 7 | 8 | for (const key in obj) { 9 | if (typeof obj[key] === "object" && obj[key] !== null) { 10 | const result = findKeyAtDepth(obj[key], targetKey, depth - 1); 11 | if (result !== undefined) { 12 | return result; 13 | } 14 | } 15 | } 16 | 17 | return undefined; 18 | } 19 | -------------------------------------------------------------------------------- /src/windows/screenshare/main.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { BrowserWindow, desktopCapturer, ipcMain, screen, session } from "electron"; 3 | import pc from "picocolors"; 4 | import { hasPipewirePulse, initVenmic, venmicList, venmicStartSystem } from "../../modules/venmic.ts"; 5 | import { dirname, getAsset } from "../../utils.ts"; 6 | 7 | let capturerWindow: BrowserWindow; 8 | const isWayland = process.platform === "linux" && (process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland" || !!process.env.WAYLAND_DISPLAY); 9 | if (isWayland) console.log(`You are using ${pc.greenBright("Wayland")}! >ᴗ<`); 10 | 11 | export function registerScreenshareHandler() { 12 | const primaryDisplay = screen.getPrimaryDisplay(); 13 | const { width, height } = primaryDisplay.workAreaSize; 14 | 15 | session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { 16 | const sources = await desktopCapturer.getSources({ 17 | types: ["screen", "window"], 18 | }); 19 | if (!sources) return callback({}); 20 | 21 | sources.map((source) => { 22 | if (!source.name) source.name = "unknown"; 23 | }); 24 | 25 | capturerWindow = new BrowserWindow({ 26 | width: width, 27 | height: height, 28 | transparent: true, 29 | resizable: false, 30 | frame: false, 31 | autoHideMenuBar: true, 32 | webPreferences: { 33 | sandbox: false, 34 | preload: path.join(dirname(), "windows/screenshare/preload.mjs"), 35 | }, 36 | }); 37 | capturerWindow.center(); 38 | capturerWindow.maximize(); 39 | 40 | await capturerWindow.loadURL(`file://${getAsset("html/picker.html")}`); 41 | capturerWindow.webContents.send("getSources", sources); 42 | 43 | ipcMain.handleOnce("selectScreenshareSource", async (_event, id, name, audio, contentHint, resolution, framerate) => { 44 | capturerWindow.close(); 45 | if (!id) return callback({}); 46 | 47 | // src/window/main/screenshare.ts 48 | await request.frame?.executeJavaScript(` 49 | window.screenshareSettings = ${JSON.stringify({resolution: resolution, framerate: framerate, contentHint: contentHint})}; 50 | `); 51 | 52 | const result = isWayland || id === "0" ? sources[0] : { id, name, width: 9999, height: 9999 }; 53 | if (audio) { 54 | if (process.platform === "linux") { 55 | await initVenmic(); 56 | if (hasPipewirePulse) { 57 | console.log(pc.cyan("[Screenshare]"), "Starting Venmic..."); 58 | console.log( 59 | pc.cyan("[Screenshare]"), 60 | "Available sources:", 61 | // Comment out "map" if you need more details for Venmic poking 62 | venmicList().map((s) => s["media.class"] === "Stream/Output/Audio" ? s["application.name"] : undefined).filter(Boolean), 63 | ); 64 | venmicStartSystem(); 65 | callback({ video: result }); 66 | return; 67 | } 68 | } 69 | callback({ video: result, audio: "loopback" }); 70 | return; 71 | } 72 | callback({ video: result }); 73 | }); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/windows/screenshare/preload.mts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from "electron"; 2 | 3 | interface IPCSources { 4 | id: string; 5 | name: string; 6 | thumbnail: HTMLCanvasElement; 7 | } 8 | 9 | function createSourceItem({ id, name, thumbnail }: IPCSources): HTMLLIElement { 10 | const li = document.createElement("li"); 11 | li.classList.add("desktop-capturer-selection__item"); 12 | 13 | const button = document.createElement("button"); 14 | button.classList.add("desktop-capturer-selection__btn"); 15 | button.dataset.id = id; 16 | button.title = name; 17 | 18 | const thumbnailContainer = document.createElement("div"); 19 | thumbnailContainer.classList.add("desktop-capturer-selection__thumbnail-container"); 20 | 21 | const img = document.createElement("img"); 22 | img.classList.add("desktop-capturer-selection__thumbnail"); 23 | img.src = thumbnail.toDataURL(); 24 | img.alt = name; 25 | 26 | const span = document.createElement("span"); 27 | span.classList.add("desktop-capturer-selection__name"); 28 | span.textContent = name; 29 | 30 | thumbnailContainer.appendChild(img); 31 | 32 | button.appendChild(thumbnailContainer); 33 | button.appendChild(span); 34 | 35 | li.appendChild(button); 36 | 37 | return li; 38 | } 39 | 40 | async function selectSource(id: string | null, title: string | null) { 41 | try { 42 | const audio = (document.getElementById("audio-checkbox") as HTMLInputElement).checked; 43 | const contentHint = (document.getElementById("content-hint-select") as HTMLInputElement).value; 44 | const resolution = (document.getElementById("resolution-textbox") as HTMLInputElement).value; 45 | const framerate = (document.getElementById("framerate-textbox") as HTMLInputElement).value; 46 | void ipcRenderer.invoke("flashTitlebar", "#5865F2"); 47 | 48 | void ipcRenderer.invoke("config:setConfig", "screensharePreviousSettings", [+resolution, +framerate, audio, contentHint]); 49 | 50 | void ipcRenderer.invoke("selectScreenshareSource", id, title, audio, contentHint, resolution, framerate); 51 | } catch (err) { 52 | console.error(err); 53 | } 54 | } 55 | 56 | async function addDisplays() { 57 | ipcRenderer.once("getSources", (_event, arg) => { 58 | const sources: IPCSources[] = arg; 59 | console.log(sources); 60 | 61 | const closeButton = document.createElement("button"); 62 | closeButton.classList.add("closeButton"); 63 | closeButton.addEventListener("click", () => { 64 | ipcRenderer.invoke("selectScreenshareSource"); 65 | }); 66 | 67 | const previousSettings = ipcRenderer.sendSync("config:getConfig", "screensharePreviousSettings"); 68 | 69 | const selectionElem = document.createElement("div"); 70 | selectionElem.classList.add("desktop-capturer-selection"); 71 | selectionElem.innerHTML = ` 72 |

${ipcRenderer.sendSync("localization:i", "screenshare-screenshare")}

73 |
74 |
    75 | ${sources 76 | .map(createSourceItem) 77 | .map((li) => li.outerHTML) 78 | .join("")} 79 |
80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 | 91 | 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 | `; 103 | 104 | document.body.appendChild(closeButton); 105 | document.body.appendChild(selectionElem); 106 | 107 | // Attach event listeners after elements are added to the DOM 108 | for (const button of document.querySelectorAll(".desktop-capturer-selection__btn")) { 109 | button.addEventListener("click", () => selectSource(button.getAttribute("data-id"), button.getAttribute("title"))); 110 | } 111 | }); 112 | } 113 | 114 | void addDisplays(); 115 | 116 | document.addEventListener("keydown", (key) => { 117 | if (key.code === "Escape") { 118 | void ipcRenderer.invoke("selectScreenshareSource"); 119 | } else if (key.code === "Enter") { 120 | void selectSource("0", "Screen"); 121 | } 122 | }); 123 | -------------------------------------------------------------------------------- /src/windows/settings/cloud/cloud.ts: -------------------------------------------------------------------------------- 1 | import { dialog } from "electron"; 2 | import pc from "picocolors"; 3 | import { cachedConfig, getConfig, setConfigBulk } from "../../../config.ts"; 4 | import type { ConfigKey, ConfigValue } from "../../../configTypes"; 5 | import { encryptionPasswords } from "../../../modules/messageEncryption.ts"; 6 | import { decryptSafeStorage } from "../../../utils.ts"; 7 | import { decryptString, encryptString } from "./encryption.ts"; 8 | import { deleteToken, getCloudHost, getCloudToken } from "./token.ts"; 9 | 10 | export const LOG_PREFIX = pc.cyanBright("[Cloud]"); 11 | export const ENDPOINT_VERSION = "v1/"; // A slash should be at the end 12 | 13 | function getEncryptionKey(): string { 14 | try { 15 | return decryptSafeStorage(getConfig("cloudEncryptionKey")); 16 | } catch (e) { 17 | return ""; 18 | } 19 | } 20 | 21 | export async function loadCloud() { 22 | const response = await callEndpoint("load", "GET"); 23 | if (!response?.settings || response.settings.length < 50) { 24 | await showDialogAndLog("info", "Cloud Settings", "Nothing to load"); 25 | return; 26 | } 27 | 28 | const cloudSettings = await decryptString(response.settings, getEncryptionKey()); 29 | if (!cloudSettings) return; 30 | console.log(LOG_PREFIX, "Loading cloud settings:", cloudSettings); 31 | 32 | const configToSet = new Map(cachedConfig); 33 | for (const [key, value] of Object.entries(cloudSettings)) { 34 | configToSet.set(key as ConfigKey, value as ConfigValue); 35 | } 36 | await setConfigBulk(configToSet); 37 | await showDialogAndLog("info", "Settings loaded", "Settings loaded from cloud successfully. Please restart GoofCord to apply the changes."); 38 | } 39 | 40 | export async function saveCloud(silent = false) { 41 | const excludedOptions = ["cloudEncryptionKey", "cloudHost", "cloudToken", "modEtagCache"]; 42 | const configToSave = new Map(cachedConfig); 43 | if (getEncryptionKey()) { 44 | configToSave.set("encryptionPasswords", encryptionPasswords); 45 | } else { 46 | excludedOptions.push("encryptionPasswords"); 47 | } 48 | 49 | const settings = Object.fromEntries( 50 | [...configToSave].filter(([key]) => !excludedOptions.includes(key)), // Removing excluded options 51 | ); 52 | const encryptedSettings = await encryptString(JSON.stringify(settings), getEncryptionKey()); 53 | if (!encryptedSettings) return; 54 | 55 | const response = await callEndpoint("save", "POST", JSON.stringify({ settings: encryptedSettings })); 56 | if (!response) return; 57 | if (!silent) await showDialogAndLog("info", "Settings saved", "Settings saved successfully on cloud."); 58 | } 59 | 60 | export async function deleteCloud() { 61 | const response = await callEndpoint("delete", "GET"); 62 | if (!response) return; 63 | await deleteToken(); 64 | await showDialogAndLog("info", "Settings deleted", "Settings deleted from cloud successfully."); 65 | } 66 | 67 | async function callEndpoint(endpoint: string, method: string, body?: string) { 68 | try { 69 | console.log(LOG_PREFIX, "Calling endpoint:", endpoint); 70 | const response = await fetch(getCloudHost() + ENDPOINT_VERSION + endpoint, { 71 | method, 72 | headers: { 73 | "Content-Type": "application/json", 74 | Authorization: await getCloudToken(), 75 | }, 76 | body, 77 | }); 78 | 79 | console.log(LOG_PREFIX, "Received server response:", await response.clone().text()); 80 | const responseJson = await response.json(); 81 | if (responseJson.error) throw new Error(responseJson.error); 82 | return responseJson; 83 | } catch (error: unknown) { 84 | const errorMessage = error instanceof Error ? error.message : String(error); 85 | if (errorMessage.includes("Unauthorized")) await deleteToken(); 86 | await showDialogAndLog("error", "Cloud error", `Error when calling "${endpoint}" endpoint: ${errorMessage}`); 87 | return undefined; 88 | } 89 | } 90 | 91 | export async function showDialogAndLog(type: "info" | "error", title: string, message: string) { 92 | type === "info" ? console.log(LOG_PREFIX, message) : console.error(LOG_PREFIX, message); 93 | await dialog.showMessageBox({ 94 | type, 95 | title, 96 | message, 97 | buttons: ["OK"], 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /src/windows/settings/cloud/encryption.ts: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto"; 2 | import util from "node:util"; 3 | import zlib from "node:zlib"; 4 | import { showDialogAndLog } from "./cloud.ts"; 5 | 6 | // Using brotli compression cuts the output in half so why not use it 7 | const brotliCompress = util.promisify(zlib.brotliCompress); 8 | const brotliDecompress = util.promisify(zlib.brotliDecompress); 9 | 10 | export async function encryptString(string: string, password: string) { 11 | try { 12 | const compressedSettings = await brotliCompress(Buffer.from(string, "utf8")); 13 | if (!password) return compressedSettings.toString("base64"); 14 | 15 | // Derive a 32-byte key for AES-256 16 | const key = crypto.createHash("sha256").update(password).digest(); 17 | const iv = crypto.randomBytes(16); 18 | const cipher = crypto.createCipheriv("aes-256-ctr", key, iv); 19 | let encrypted = cipher.update(compressedSettings); 20 | encrypted = Buffer.concat([encrypted, cipher.final()]); 21 | 22 | const resultBuffer = Buffer.concat([iv, encrypted]); 23 | return resultBuffer.toString("base64"); 24 | } catch (e) { 25 | await showDialogAndLog("error", "Encryption error", `Failed to encrypt settings: ${e}`); 26 | return undefined; 27 | } 28 | } 29 | 30 | export async function decryptString(encryptedStr: string, password: string) { 31 | try { 32 | let decrypted: Buffer; 33 | if (password) { 34 | // Derive a 32-byte key for AES-256 35 | const key = crypto.createHash("sha256").update(password).digest(); 36 | const encryptedBuffer = Buffer.from(encryptedStr, "base64"); 37 | const iv = encryptedBuffer.subarray(0, 16); 38 | const encryptedData = encryptedBuffer.subarray(16); 39 | 40 | const decipher = crypto.createDecipheriv("aes-256-ctr", key, iv); 41 | decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]); 42 | } else { 43 | decrypted = Buffer.from(encryptedStr, "base64"); 44 | } 45 | 46 | const decompressedSettings = await brotliDecompress(decrypted); 47 | // Parsing here and not in cloud.ts to catch parsing errors too 48 | return JSON.parse(decompressedSettings.toString("utf8")); 49 | } catch (e) { 50 | await showDialogAndLog("error", "Decryption error", `Failed to decrypt settings. Is the password correct?\n${e}`); 51 | return undefined; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/windows/settings/main.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { BrowserWindow, ipcMain, shell } from "electron"; 3 | import { i, initLocalization } from "../../modules/localization.ts"; 4 | import { dirname, getAsset, getCustomIcon, getDisplayVersion, userDataPath } from "../../utils.ts"; 5 | import type { Config } from "../../configTypes.d.ts"; 6 | import { cachedConfig, getConfig } from "../../config.ts"; 7 | import { saveCloud } from "./cloud/cloud.ts"; 8 | 9 | export let settingsWindow: BrowserWindow; 10 | let isOpen = false; 11 | let originalConfig: Config; 12 | 13 | ipcMain.handle("openFolder", async (_event, folder: string) => await shell.openPath(path.join(userDataPath, `/${folder}/`))); 14 | 15 | export async function createSettingsWindow() { 16 | if (isOpen) { 17 | settingsWindow.show(); 18 | settingsWindow.restore(); 19 | return; 20 | } 21 | 22 | originalConfig = { ...cachedConfig }; 23 | 24 | console.log("Creating a settings window."); 25 | settingsWindow = new BrowserWindow({ 26 | width: 660, 27 | height: 700, 28 | title: i("settingsWindow-title") + getDisplayVersion(), 29 | darkTheme: true, 30 | frame: true, 31 | icon: getCustomIcon(), 32 | backgroundColor: "#2f3136", 33 | autoHideMenuBar: true, 34 | webPreferences: { 35 | sandbox: false, 36 | preload: path.join(dirname(), "windows/settings/preload.mjs"), 37 | nodeIntegrationInSubFrames: false, 38 | }, 39 | }); 40 | isOpen = true; 41 | 42 | settingsWindow.webContents.setWindowOpenHandler(({ url }) => { 43 | shell.openExternal(url); 44 | return { action: "deny" }; 45 | }); 46 | 47 | await settingsWindow.loadURL(`file://${getAsset("html/settings.html")}`); 48 | 49 | settingsWindow.on("close", async (event) => { 50 | isOpen = false; 51 | if (originalConfig !== cachedConfig && getConfig("autoSaveCloud")) { 52 | event.preventDefault(); 53 | try { 54 | await saveCloud(true); 55 | settingsWindow.destroy(); // Not trigger close event 56 | } catch (error) { 57 | console.error("Error saving config before closing:", error); 58 | settingsWindow.destroy(); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | export async function hotreloadLocale() { 65 | await initLocalization(); 66 | if (settingsWindow) settingsWindow.webContents.reload(); 67 | } -------------------------------------------------------------------------------- /src/windows/settings/preload.mts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron"; 2 | import type { ConfigKey, ConfigValue } from "../../configTypes"; 3 | import { type SettingEntry, settingsSchema } from "../../settingsSchema.ts"; 4 | import { findKeyAtDepth } from "../preloadUtils.ts"; 5 | import { renderSettings } from "./settingsRenderer.ts"; 6 | 7 | console.log("GoofCord Settings"); 8 | 9 | contextBridge.exposeInMainWorld("settings", { 10 | loadCloud: () => ipcRenderer.invoke("cloud:loadCloud"), 11 | deleteCloud: () => ipcRenderer.invoke("cloud:deleteCloud"), 12 | saveCloud: () => ipcRenderer.invoke("cloud:saveCloud"), 13 | openFolder: (folder: string) => ipcRenderer.invoke("openFolder", folder), 14 | clearCache: () => ipcRenderer.invoke("cacheManager:clearCache"), 15 | }); 16 | 17 | const settingsData: Record = {}; 18 | const elementsWithShowAfter: [string, HTMLElement][] = []; 19 | 20 | async function initSettings() { 21 | while (document.body === null) await new Promise((resolve) => setTimeout(resolve, 10)); 22 | await renderSettings(); 23 | 24 | const elements = document.querySelectorAll("[setting-name]"); 25 | for (const element of elements) { 26 | const name = element.getAttribute("setting-name"); 27 | if (!name) continue; 28 | 29 | const revertButton = element.parentElement?.firstElementChild; 30 | revertButton?.addEventListener("click", () => revertSetting(element)); 31 | element.addEventListener("change", () => saveSettings(element)); 32 | 33 | const settingData = findKeyAtDepth(settingsSchema, name, 2); 34 | if (settingData?.showAfter) elementsWithShowAfter.push([name, element]); 35 | settingsData[name] = settingData; 36 | } 37 | } 38 | 39 | async function saveSettings(changedElement: HTMLElement) { 40 | const settingName = changedElement.getAttribute("setting-name"); 41 | if (!settingName) return; 42 | 43 | const settingData = settingsData[settingName]; 44 | let settingValue = await getSettingValue(changedElement, settingName as ConfigKey); 45 | if (settingValue === undefined) return; 46 | if (settingData.encrypted) settingValue = encryptSetting(settingValue); 47 | 48 | void ipcRenderer.invoke("config:setConfig", settingName, settingValue); 49 | updateVisibility(settingName, settingValue); 50 | void ipcRenderer.invoke("flashTitlebar", "#5865F2"); 51 | 52 | if (settingData.onChange) void ipcRenderer.invoke(settingData.onChange); 53 | } 54 | 55 | function updateVisibility(changedElementName: string, changedElementValue: unknown) { 56 | for (const [name, element] of elementsWithShowAfter) { 57 | const settingData = settingsData[name]; 58 | if (settingData?.showAfter && settingData.showAfter.key === changedElementName) { 59 | const shouldShow = evaluateShowAfter(settingData.showAfter.condition, changedElementValue); 60 | element.closest("fieldset")?.classList.toggle("hidden", !shouldShow); 61 | } 62 | } 63 | } 64 | 65 | export function evaluateShowAfter(condition: (value: unknown) => boolean, value: unknown) { 66 | return condition(value); 67 | } 68 | 69 | async function getSettingValue(element: HTMLElement, settingName: K): Promise | undefined> { 70 | try { 71 | if (element instanceof HTMLInputElement) { 72 | if (element.type === "checkbox") return element.checked as ConfigValue; 73 | if (element.type === "text") { 74 | if (element.dataset.hidden) return JSON.parse(element.value) as ConfigValue; 75 | return element.value as ConfigValue; 76 | } 77 | // Horror 78 | if (element.type === "file") { 79 | const file = element.files?.[0]; 80 | if (!file) throw new Error("No file selected"); 81 | 82 | return await new Promise((resolve, reject) => { 83 | const reader = new FileReader(); 84 | reader.onload = async (event) => { 85 | const fileContent = event.target?.result; 86 | if (!fileContent) return reject(new Error("No file content")); 87 | if (typeof fileContent === "string") return reject(new Error("File content is a string")); 88 | 89 | try { 90 | const result = await ipcRenderer.invoke("utils:saveFileToGCFolder", settingName, Buffer.from(new Uint8Array(fileContent))); 91 | resolve(result as ConfigValue); 92 | } catch (ipcError) { 93 | reject(ipcError); 94 | } 95 | }; 96 | reader.onerror = (error) => reject(new Error("Error reading file: " + error)); 97 | reader.readAsArrayBuffer(file); 98 | }); 99 | } 100 | } else if (element instanceof HTMLSelectElement) { 101 | return (element.multiple ? Array.from(element.selectedOptions).map((option) => option.value) : element.value) as ConfigValue; 102 | } else if (element instanceof HTMLTextAreaElement) { 103 | return createArrayFromTextarea(element.value) as ConfigValue; 104 | } 105 | throw new Error(`Unsupported element type for: ${settingName}`); 106 | } catch (error) { 107 | console.error(`Failed to get ${settingName}'s value:`, error); 108 | return undefined; 109 | } 110 | } 111 | 112 | export async function revertSetting(setting: HTMLElement) { 113 | const elementName = setting.getAttribute("setting-name"); 114 | if (!elementName) return; 115 | 116 | const defaultValue = settingsData[elementName]?.defaultValue; 117 | 118 | if (setting instanceof HTMLInputElement) { 119 | if (setting.type === "checkbox" && typeof defaultValue === "boolean") { 120 | setting.checked = defaultValue; 121 | } else if (setting.type === "file") { 122 | void ipcRenderer.invoke("config:setConfig", elementName, defaultValue); 123 | void ipcRenderer.invoke("flashTitlebar", "#5865F2"); 124 | return; 125 | } else if (typeof defaultValue === "string") { 126 | setting.value = defaultValue; 127 | } 128 | } else if (setting instanceof HTMLTextAreaElement && typeof defaultValue === "string") { 129 | setting.value = Array.isArray(defaultValue) ? defaultValue.join(",\n") : defaultValue; 130 | } 131 | 132 | await saveSettings(setting); 133 | } 134 | 135 | function createArrayFromTextarea(input: string): string[] { 136 | return input 137 | .split(/[\r\n,]+/) 138 | .map((item) => item.trim()) 139 | .filter(Boolean); 140 | } 141 | 142 | export function encryptSetting(settingValue: ConfigValue) { 143 | if (typeof settingValue === "string") { 144 | return ipcRenderer.sendSync("utils:encryptSafeStorage", settingValue); 145 | } 146 | if (Array.isArray(settingValue)) { 147 | return settingValue.map((value: unknown) => ipcRenderer.sendSync("utils:encryptSafeStorage", value)); 148 | } 149 | return settingValue; 150 | } 151 | 152 | export function decryptSetting(settingValue: ConfigValue) { 153 | if (typeof settingValue === "string") { 154 | return ipcRenderer.sendSync("utils:decryptSafeStorage", settingValue); 155 | } 156 | if (Array.isArray(settingValue)) { 157 | return settingValue.map((value: unknown) => ipcRenderer.sendSync("utils:decryptSafeStorage", value)); 158 | } 159 | return settingValue; 160 | } 161 | 162 | initSettings().catch(console.error); 163 | -------------------------------------------------------------------------------- /src/windows/settings/settingsRenderer.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, webFrame } from "electron"; 2 | import type { ConfigKey, ConfigValue } from "../../configTypes.d.ts"; 3 | import { type ButtonEntry, type SettingEntry, settingsSchema } from "../../settingsSchema.ts"; 4 | import { decryptSetting, evaluateShowAfter } from "./preload.mts"; 5 | 6 | function sanitizeForId(name: string): string { 7 | return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); 8 | } 9 | 10 | export async function renderSettings() { 11 | const categoryKeys = Object.keys(settingsSchema); 12 | 13 | let panelHtml = ""; 14 | categoryKeys.forEach((categoryName, index) => { 15 | const panelId = `panel-${sanitizeForId(categoryName)}`; 16 | const isActive = index === 0 ? "active" : ""; 17 | const { settingsHtml, buttonsHtml } = generatePanelInnerContent(categoryName); 18 | 19 | panelHtml += ` 20 |
21 | 22 |
23 | ${settingsHtml} 24 | ${buttonsHtml ? `
${buttonsHtml}
` : ""} 25 |
26 |
27 | `; 28 | }); 29 | 30 | let tabHtml = ""; 31 | categoryKeys.forEach((categoryName, index) => { 32 | const panelId = `panel-${sanitizeForId(categoryName)}`; 33 | const isActive = index === 0 ? "active" : ""; 34 | const categoryTitle = ipcRenderer.sendSync("localization:i", `category-${categoryName.toLowerCase().split(" ")[0]}`); 35 | 36 | tabHtml += ` 37 | 38 | `; 39 | }); 40 | 41 | document.body.innerHTML = ` 42 |
43 |
44 | 47 |
48 | 49 | ${ipcRenderer.sendSync("utils:isEncryptionAvailable") ? "" : ` 50 |
51 |

${ipcRenderer.sendSync("localization:i", "settings-encryption-unavailable")}

52 |
53 | `} 54 | 55 |
56 | ${panelHtml} 57 |
58 |
59 | `; 60 | 61 | void webFrame.executeJavaScript("window.initSwitcher(); window.initMultiselect();"); 62 | } 63 | 64 | function generatePanelInnerContent(categoryName: string): { settingsHtml: string, buttonsHtml: string } { 65 | let buttonsHtml = ""; 66 | let settingsHtml = ""; 67 | 68 | for (const [setting, entry] of Object.entries(settingsSchema[categoryName])) { 69 | if (setting.startsWith("button-")) { 70 | buttonsHtml += createButton(setting, entry as ButtonEntry); 71 | } else { 72 | settingsHtml += createSetting(setting as ConfigKey, entry as SettingEntry); 73 | } 74 | } 75 | 76 | return { settingsHtml, buttonsHtml }; 77 | } 78 | 79 | function createSetting(setting: ConfigKey, entry: SettingEntry): string | "" { 80 | let value: ConfigValue = ipcRenderer.sendSync("config:getConfig", setting); 81 | 82 | if (entry.encrypted && typeof value === 'string') { 83 | value = decryptSetting(value); 84 | } 85 | 86 | let isHidden = false; 87 | if (!entry.name) { 88 | isHidden = true; 89 | entry.inputType = "textfield"; 90 | } 91 | if (entry.showAfter) { 92 | const controllingValue = ipcRenderer.sendSync("config:getConfig", entry.showAfter.key as ConfigKey); 93 | isHidden = !evaluateShowAfter(entry.showAfter.condition, controllingValue); 94 | } 95 | 96 | const name = ipcRenderer.sendSync("localization:i", `opt-${setting}`) ?? setting; 97 | const description = ipcRenderer.sendSync("localization:i", `opt-${setting}-desc`) ?? ""; 98 | 99 | return ` 100 |
101 |
102 |
103 | ${getInputElement(entry, setting, value)} 104 | 105 |
106 |

${description}

107 |
108 | `; 109 | } 110 | 111 | function createButton(id: string, entry: ButtonEntry): string { 112 | const buttonText = ipcRenderer.sendSync("localization:i", `opt-${id}`); 113 | return ``; 114 | } 115 | 116 | 117 | function getInputElement(entry: SettingEntry, setting: ConfigKey, value: ConfigValue): string { 118 | if (!entry.name) { 119 | return ``; 120 | } 121 | 122 | switch (entry.inputType) { 123 | case "checkbox": 124 | return ``; 125 | case "textfield": 126 | return ``; 127 | case "textarea": { 128 | const textValue = Array.isArray(value) ? value.join(",\n") : String(value); 129 | return ``; 130 | } 131 | case "file": 132 | return ``; 133 | case "dropdown": 134 | case "dropdown-multiselect": { 135 | const isMultiselect = entry.inputType === "dropdown-multiselect"; 136 | const selectValue = Array.isArray(value) ? value : [String(value)]; 137 | return ` 138 | 150 | `; 151 | } 152 | default: 153 | console.warn(`Unsupported input type: ${entry.inputType} for setting ${setting}`); 154 | return `Unsupported input type: ${entry.inputType}`; 155 | } 156 | } 157 | 158 | function escapeHtmlValue(unsafeString: string) { 159 | return unsafeString 160 | .replace(/&/g, "&") 161 | .replace(/"/g, """) 162 | .replace(//g, ">"); 164 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // Reference: https://www.typescriptlang.org/tsconfig 2 | { 3 | "include": ["src/**/*", "build/**/*"], 4 | //"exclude": ["src/**/*.test.ts"], 5 | "compilerOptions": { 6 | "rootDir": "./", 7 | "outDir": "ts-out", 8 | "moduleResolution": "bundler", 9 | 10 | "strict": true, 11 | "noImplicitAny": false, 12 | "noImplicitReturns": false, 13 | "noFallthroughCasesInSwitch": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "esModuleInterop": true, 16 | "resolveJsonModule": true, 17 | "allowImportingTsExtensions": true, 18 | "noEmit": true, 19 | "lib": ["ESNext", "dom", "dom.Iterable"], 20 | 21 | "module": "esnext", 22 | "removeComments": true, 23 | "inlineSourceMap": true, 24 | "inlineSources": true, 25 | 26 | "declaration": false, 27 | "declarationMap": false, 28 | 29 | "target": "esnext", 30 | "downlevelIteration": false, 31 | "importHelpers": false 32 | } 33 | } 34 | --------------------------------------------------------------------------------