Updates and Linting (#578)

* Updates and Linting

* fix lint task

* [ImgBot] Optimize images

*Total -- 404.98kb -> 304.38kb (24.84%)

/assets/screenshot-1920x1080.png -- 205.00kb -> 123.21kb (39.9%)
/build/appx/Square150x150Logo.png -- 9.63kb -> 7.71kb (20%)
/assets/ac_icon.png -- 40.15kb -> 34.98kb (12.88%)
/assets/StoreLogo.png -- 40.15kb -> 34.98kb (12.88%)
/assets/Square150x150Logo.png -- 7.24kb -> 6.53kb (9.83%)
/assets/ac_icon_transparent.png -- 45.54kb -> 42.00kb (7.76%)
/assets/ac_plug_colored.png -- 17.98kb -> 16.72kb (7%)
/assets/ac_black_plug.png -- 8.49kb -> 8.06kb (5.06%)
/assets/ac_black_plug_hollow.png -- 10.30kb -> 9.95kb (3.4%)
/build/appx/Square44x44Logo.png -- 1.69kb -> 1.64kb (2.89%)
/assets/Square44x44Logo.png -- 1.90kb -> 1.85kb (2.83%)
/assets/Wide310x150Logo.png -- 4.21kb -> 4.17kb (0.97%)
/build/appx/Wide310x150Logo.png -- 4.21kb -> 4.17kb (0.97%)
/assets/ac_white_plug.png -- 8.49kb -> 8.42kb (0.83%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

* Asyncification!!!
Check `REVIEW` comments

* More async for `src/discord`

* update packages to latest minor version

* Void some promises

* Add some types - 93 problems left!

* make DeepScan Happy

* DeepScan part 2

* I am the Constant

* preload must be `.mts`

* Migrate electron context menu official package

* fix bad preload on setup window

* fix minor import oversights

* fix modloader

* Stop main window from continuing during setup

* update packages, slow dependabot

* Remove paste override, it seems to work without now

* IPC typing

* Package updates & a few more typings

* fix linting errors in screenshare

* use pnpm in actions

* fix dev releaser?

* update node build, fix dev one more time

* release action is broke

* Fix Release

* update actions

* actions are so finicky

* remove delete-tag-and-release

* add github token env

* Hopefully this fixes the release workflow

* [debug]

* this should actually fix it

* Fix typo in dev action

* put everything in a dir and then get it

* use a different releaser

* correct release file location

* action places it in a folder named x.zip, recurse into that and grab the actual files

* Cleanup actions a bit

* release is dependent on mac build

* remove mac build

* split linux arm and x86

* rely on linux arm

* remove deprecated action

* attempt to fix weird recursive zip

* fix env

* use pnpm in actions

fix dev releaser?

update node build, fix dev one more time

release action is broke

Fix Release

update actions

actions are so finicky

remove delete-tag-and-release

add github token env

Hopefully this fixes the release workflow

[debug]

this should actually fix it

Fix typo in dev action

put everything in a dir and then get it

use a different releaser

correct release file location

action places it in a folder named x.zip, recurse into that and grab the actual files

Cleanup actions a bit

release is dependent on mac build

remove mac build

split linux arm and x86

rely on linux arm

remove deprecated action

attempt to fix weird recursive zip

fix env

* don't globally install pnpm packages (I don't think the cache checks global)

* Type the armcord window

* Finalize typings

* fix deepscan issues

* fix screenshare preload

* fix app quitting

---------

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: smartfrigde <37928912+smartfrigde@users.noreply.github.com>
This commit is contained in:
Aiden 2024-06-14 08:57:34 -04:00 committed by GitHub
parent 77416701a4
commit 10b7e638de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 2571 additions and 1631 deletions

View file

@ -1,35 +0,0 @@
extends: eslint-config-dmitmel/presets/node
env:
browser: true
plugins: ["prettier"]
settings:
node:
tryExtensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".node"]
ignorePatterns: ["src/arrpc/**"]
rules:
prettier/prettier:
- error
node/no-unsupported-features/es-syntax:
- error
- ignores:
- modules
overrides:
- files: "**/*.ts*"
extends:
- eslint-config-dmitmel/presets/typescript-addon
parserOptions:
project: "tsconfig.json"
sourceType: module
rules:
eqeqeq: 0
require-await: 0
no-undefined: 0
node/no-unsupported-features/es-syntax: 0
"@typescript-eslint/no-dynamic-delete": 0
"@typescript-eslint/no-explicit-any": 0
"@typescript-eslint/no-non-null-asserted-optional-chain": 0
"@typescript-eslint/naming-convention": 0

View file

@ -3,7 +3,7 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
- package-ecosystem: npm
directory: "/"
schedule:

1
.github/release.md vendored
View file

@ -1,3 +1,4 @@
# Thanks for checking out ArmCord dev builds!
These builds are unstable and not ready for full release. They contain new experimental features and changes. We provide no official support for them.
Make sure to join our [Discord server](https://discord.gg/uaW5vMY3V6) to share opinions, or to chat with ArmCord developers!

View file

@ -12,174 +12,136 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i
- name: Build
run: npm run build && electron-builder --linux zip && electron-builder --arm64 --linux zip
run: pnpm run build && pnpm electron-builder --linux zip
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordLinux.zip
path: dist/ArmCord-3.3.0.zip
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: ArmCordLinuxArm64.zip
path: dist/ArmCord-3.3.0-arm64.zip
build-snap:
name: ArmCordLinux
path: dist/armcord-*.zip
build-linux-arm:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i
- name: Build
run: npm run build && electron-builder --linux snap --config.snap.grade=devel
run: pnpm run build && pnpm electron-builder --arm64 --linux zip
- uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
snap: dist/ArmCord_3.3.0_amd64.snap
release: edge
name: ArmCordLinuxArm64
path: dist/armcord-*-arm64.zip
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i
- name: Build
run: npm run build && electron-builder --windows zip
run: pnpm run build && pnpm electron-builder --windows zip
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordWindows.zip
path: dist/ArmCord-3.3.0-win.zip
build-windowsOnARM:
name: ArmCordWindows
path: dist/armcord-3.3.0-win.zip
build-windows-arm:
runs-on: windows-latest
steps:
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Set architecture
run: set npm_config_arch=arm64
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i
- name: Build
run: npm run build && electron-builder --windows zip --arm64
run: pnpm run build && pnpm electron-builder --windows zip --arm64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordWindowsArm64.zip
path: dist\ArmCord-3.3.0-arm64-win.zip
name: ArmCordWindowsArm64
path: dist\armcord-3.3.0-arm64-win.zip
release:
runs-on: ubuntu-latest
needs: [build-linux, build-windows, build-windowsOnARM]
needs: [build-linux, build-windows, build-windows-arm, build-linux-arm]
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with:
name: ArmCordWindows.zip
path: windows
path: release-files
- uses: actions/download-artifact@v2
with:
name: ArmCordLinux.zip
path: linux
- uses: actions/download-artifact@v2
with:
name: ArmCordLinuxArm64.zip
path: linux
- uses: actions/download-artifact@v2
with:
name: ArmCordWindowsArm64.zip
path: windows
- name: Get some values needed for the release
- name: Get short commit hash
id: vars
shell: bash
run: |
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
run: echo "sha_short=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_OUTPUT
- uses: dev-drprasad/delete-tag-and-release@v0.2.1
with:
delete_release: true
tag_name: devbuild
- run: gh release delete devbuild -y --cleanup-tag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create the release
uses: softprops/action-gh-release@v1
- name: Create Release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: devbuild
bodyFile: .github/release.md
generateReleaseNotes: true
name: Dev Build ${{ steps.vars.outputs.sha_short }}
draft: false
prerelease: true
body_path: .github/release.md
files: |
linux/ArmCord-3.3.0.zip
linux/ArmCord-3.3.0-arm64.zip
windows/ArmCord-3.3.0-win.zip
windows/ArmCord-3.3.0-arm64-win.zip
tag: devbuild
artifacts: "release-files/**/*.zip"

32
.github/workflows/eslint.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Lint
on:
push:
branches:
- "*"
pull_request:
branches:
- "*"
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install Node.js dependencies
run: pnpm install
- name: Run linters
run: pnpm run lint

View file

@ -13,69 +13,67 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i -g cargo-cp-artifact electron-builder && pnpm i
- name: Build
run: npm run build && electron-builder --linux && electron-builder --arm64 --linux && electron-builder --armv7l --linux
run: pnpm run build && electron-builder --linux && electron-builder --arm64 --linux && electron-builder --armv7l --linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: List all files in the dist directory
run: ls -l dist
- name: Delete unpacked builds
run: rm -rf dist/linux-unpacked && rm -rf dist/linux-arm64-unpacked && rm -rf dist/linux-armv7l-unpacked
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordLinux
path: dist/
build-mac:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i -g cargo-cp-artifact electron-builder && pnpm i
- name: Build
run: npm run build && electron-builder --macos --x64 --arm64
run: pnpm run build && electron-builder --macos --x64 --arm64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: List all files in the dist directory
run: ls -l dist
- name: Delete unpacked builds
run: rm -rf dist/macos-unpacked
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordMac
path: dist/
@ -84,62 +82,62 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: pnpm/action-setup@v4
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm i -g cargo-cp-artifact electron-builder && pnpm i
- name: Build
run: npm run build && electron-builder --windows
run: pnpm run build && electron-builder --windows
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete unpacked builds
run: Remove-Item -LiteralPath ".\dist\win-unpacked" -Force -Recurse
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordWindows
path: dist/
build-windowsOnARM:
runs-on: windows-latest
steps:
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Set architecture
run: set npm_config_arch=arm64
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
run: pnpm install -g cargo-cp-artifact electron-builder && pnpm install
- name: Build
run: npm run build && electron-builder --windows --arm64
run: pnpm run build && electron-builder --windows --arm64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete unpacked builds
run: Remove-Item -LiteralPath ".\dist\win-arm64-unpacked" -Force -Recurse
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ArmCordWindowsArm64
path: dist/
@ -148,30 +146,37 @@ jobs:
needs: [build-linux, build-mac, build-windows, build-windowsOnARM]
steps:
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with:
name: ArmCordMac
path: macos
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with:
name: ArmCordWindows
path: windows
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with:
name: ArmCordWindowsArm64
path: windows
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with:
name: ArmCordLinux
path: linux
- name: ls
run: ls
- name: Delete unwanted directories
run: rm -rf {linux,macos,windows}/*/
rm -rf {linux,macos,windows}/.icon*
rm -rf {linux,macos,windows}/builder-debug.yml
- name: ls dirs
run: ls linux && ls macos && ls windows
- name: Get some values needed for the release
id: vars
shell: bash
@ -179,7 +184,7 @@ jobs:
echo "::set-output name=releaseTag::$(git describe --tags --abbrev=0)"
- name: Create Release
uses: actions/github-script@v2
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ dist
ts-out/
ts-out
package-lock.json
.pnpm-store

View file

@ -1,5 +1,5 @@
#!/bin/sh
set -e
npm run format
pnpm run format
git add -A

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"ExodiusStudios.comment-anchors"
]
}

19
.vscode/settings.json vendored
View file

@ -1,2 +1,21 @@
{
"cSpell.words": [
"armcord",
"armcordinternal",
"arrpc",
"Autogain",
"clientmod",
"copyfiles",
"Ducko",
"modloader",
"nsis",
"smartfridge",
"smartfrigde",
"togglefullscreen",
"unmaximize",
"vaapi"
],
"cSpell.ignorePaths": [
"assets/lang"
]
}

View file

@ -34,7 +34,6 @@
ArmCord is using a newer build of Electron than the stock Discord app. This means you can have a much more stable and secure experience, along with slightly better performance.
- **Cross-platform support!**
ArmCord was originally created for ARM64 Linux devices since Discord doesn't support them. We soon decided to support every platform that [Electron supports](https://github.com/electron/electron#platform-support)!
@ -42,9 +41,11 @@
# How to run/install it?
## Packaging status
[![Packaging status](https://repology.org/badge/vertical-allrepos/armcord.svg)](https://repology.org/project/armcord/versions)
### Windows
<a href="https://microsoft.com/store/apps/9PFHLJFD7KJT">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" alt="Download ArmCord" />
</a>
@ -52,106 +53,146 @@
If you're using an older version of Windows, you need to use [pre-built installers](https://www.armcord.app/download).
### Flatpak
<a href='https://flathub.org/apps/details/xyz.armcord.ArmCord'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.svg'/></a>
### Debian, Ubuntu and Raspbian repository
ArmCord is available on our official repositories for `apt` package manager. By using this method you'll receive automatic updates and get all the dependencies. Run the following commands to install ArmCord from them:
```sh
curl -fsSL https://apt.armcord.app/public.gpg | sudo gpg --dearmor -o /usr/share/keyrings/armcord.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/armcord.gpg] https://apt.armcord.app/ stable main" | sudo tee /etc/apt/sources.list.d/armcord.list
sudo apt update
sudo apt install armcord
```
If you previously used old ArmCord apt repo, here's how you can remove it:
```sh
sudo rm /etc/apt/sources.list.d/armcord.list
sudo rm /usr/share/keyrings/armcord.gpg
sudo apt update
```
### Snap package
ArmCord is also available on the Snap store [here](https://snapcraft.io/armcord).
<a href="https://snapcraft.io/armcord">
<img alt="Get it from the Snap Store" src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg" />
</a>
Similar to `armcord-git` on AUR, you can install the latest dev builds through snaps by running this command:
```shell
sudo snap install armcord --channel=latest/edge
```
Snapd will automatically update the app including developer builds.
### Winget Package
ArmCord is also available on the [winget-pkgs](https://github.com/microsoft/winget-pkgs) repository:
```
```ps1
winget install ArmCord.ArmCord
```
### Scoop package
ArmCord is also available on [Scoop extras](https://github.com/ScoopInstaller/Extras) repo:
```
```ps1
scoop bucket add extras
```
```
```ps1
scoop install armcord
```
### AUR Package
ArmCord is also available on the Arch User Repository (AUR):
- [armcord-bin](https://aur.archlinux.org/packages/armcord-bin) - ArmCord Release ~ Static binary from release, stable release only
- [armcord-git](https://aur.archlinux.org/packages/armcord-git) - ArmCord Dev ~ Latest devbuild built from source (takes ~1 minute) using the system electron
Install it via an AUR helper tool like `yay`.
**Example:** `yay -S armcord-bin`
### Homebrew repository
ArmcCord also has a homebrew repository
```
ArmCord also has a homebrew repository
```zsh
brew tap armcord/armcord
```
```
```zsh
brew install --cask armcord
```
### FreeBSD
You can also get ArmCord running on FreeBSD by following [these instructions](https://gist.github.com/axyiee/4d29c982ac85d5d26f98a51040b5de37).
### Pi-Apps
ArmCord is also available in [Pi-Apps](https://github.com/Botspot/pi-apps).
[![badge](https://github.com/Botspot/pi-apps/blob/master/icons/badge.png?raw=true)](https://github.com/Botspot/pi-apps)
### Pre-built binaries:
Check the **releases tab** for precompiled packages for Linux, Windows, and Mac OS. Alternatively, use our Sourceforge mirror.
<a href="https://sourceforge.net/projects/armcord/files/latest/download"><img alt="Download ArmCord" src="https://a.fsdn.com/con/app/sf-download-button" width=276 height=48 srcset="https://a.fsdn.com/con/app/sf-download-button?button_size=2x 2x"></a>
### Compiling:
Alternatively, you can run ArmCord from source ([NodeJS](https://nodejs.dev), [pnpm](https://pnpm.io/installation#using-npm), and [rust toolchain](https://www.rust-lang.org/tools/install) are required):
1. Clone ArmCord repo: `git clone https://github.com/ArmCord/ArmCord.git`
2. Run `pnpm install` to install dependencies
3. Build with `npm run build`
4. Compile/Package with `npm run package`
3. Build with `pnpm run build`
4. Compile/Package with `pnpm run package`
# FAQ
## Do you have a support Discord?
[![](https://dcbadge.vercel.app/api/server/TnhxcqynZ2)](https://discord.gg/TnhxcqynZ2)
## Will I get banned for using this?
- You are breaking [Discord ToS](https://discord.com/terms#software-in-discord%E2%80%99s-services) by using ArmCord, but no one has been banned from using it or any of the client mods included.
## Can I use this on anything other than ARM?
- Yes! ArmCord should work normally under Windows, MacOS, and Linux as long as it has Electron support.
## How can I access the settings?
- Open Discord settings and there should be a button `ArmCord Settings` button with a white Discord icon, you can also right click on the tray icon and click `Open Settings`
## How does this work?
- We are using the official web app and wrapping it up in Electron. While you may think this is lame and done like thousands of times before, what makes us unique is that we actually strive for creating a customized experience. You can very easily load themes and mods with no installers/injectors. You can even make the client have transparency effects and follow the fluent design of Windows! At its core, it's just a simple web wrapper, however, we applied many patches to make this work well for you <3
## Why is macOS support lacking?
## Why is MacOS support lacking?
- Due to me not owning any macOS device, I can't easily debug/test or do anything related to it. Of course, VMs and Hackintosh machines exist but from my experience, these are unreliable or very time-consuming to set up and maintain. While ArmCord "works" on macOS you may encounter weird issues or inconsistencies with other apps in terms of how they behave (for example macOS lack of tray).
## Where can I find the source code?
- The source code is on [GitHub](https://github.com/ArmCord/ArmCord/).
## Where can I translate this?
- Translations are done using our [Weblate page](https://hosted.weblate.org/projects/armcord/armcord/).
# Credits
- [ArmCord UI design, branding, and a few features](https://github.com/kckarnige)
- [OpenAsar](https://github.com/GooseMod/OpenAsar)
- [arRPC (for Rich Presence)](https://github.com/OpenAsar/arrpc)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

48
eslint.config.js Normal file
View file

@ -0,0 +1,48 @@
/* eslint-disable n/no-unpublished-import */
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-plugin-prettier";
import n from "eslint-plugin-n";
export default tseslint.config(
eslint.configs.recommended,
{ignores: ["ts-out", "src/discord/content/js"]}, // REVIEW - investigate discord files a bit before finalizing this - I think these are meant to be run in the app console, and this would be difficult to type
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
n.configs["flat/recommended"],
{
settings: {
n: {
allowModules: ["electron"],
tryExtensions: [".tsx", ".ts", ".jsx", ".js", ".json", ".node", ".d.ts"]
}
},
plugins: {
prettier,
n
},
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
rules: {
"no-constant-binary-expression": 0,
"n/no-unsupported-features/node-builtins": 1,
"@typescript-eslint/no-unused-vars": [
2,
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_"
}
],
// @ts-expect-error - Don't worry about it
...prettier.configs.recommended.rules
}
}
);

View file

@ -1,23 +1,23 @@
{
"name": "ArmCord",
"name": "armcord",
"version": "3.3.0",
"description": "ArmCord is a custom client designed to enhance your Discord experience while keeping everything lightweight.",
"main": "ts-out/main.js",
"engines": {
"node": ">=18.0.0"
"node": ">=22"
},
"scripts": {
"build": "tsc && copyfiles -u 1 src/**/*.html src/**/**/*.css src/**/**/*.js ts-out/ && copyfiles package.json ts-out/ && copyfiles assets/**/** ts-out/",
"watch": "tsc -w",
"start": "npm run build && electron ./ts-out/main.js",
"startThemeManager": "npm run build && electron ./ts-out/main.js themes",
"startKeybindManager": "npm run build && electron ./ts-out/main.js keybinds",
"startWayland": "npm run build && electron ./ts-out/main.js --ozone-platform-hint=auto --enable-features=WebRTCPipeWireCapturer,WaylandWindowDecorations --disable-gpu",
"package": "npm run build && electron-builder",
"packageQuick": "npm run build && electron-builder --dir",
"start": "pnpm run build && electron ./ts-out/main.js",
"startThemeManager": "pnpm run build && electron ./ts-out/main.js themes",
"startKeybindManager": "pnpm run build && electron ./ts-out/main.js keybinds",
"startWayland": "pnpm run build && electron ./ts-out/main.js --ozone-platform-hint=auto --enable-features=WebRTCPipeWireCapturer,WaylandWindowDecorations --disable-gpu",
"package": "pnpm run build && electron-builder",
"packageQuick": "pnpm run build && electron-builder --dir",
"format": "prettier --write src *.json",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore",
"CIbuild": "npm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip",
"lint": "eslint \"**/*.{ts,tsx,js,jsx}\" .",
"CIbuild": "pnpm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip",
"prepare": "git config --local core.hooksPath .hooks/"
},
"repository": {
@ -32,27 +32,27 @@
},
"homepage": "https://github.com/armcord/armcord#readme",
"devDependencies": {
"@types/node": "^20.11.1",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@eslint/js": "^9.4.0",
"@types/eslint__js": "^8.42.3",
"@types/node": "^20.14.2",
"@types/ws": "^8.5.10",
"copyfiles": "^2.4.1",
"electron": "30.0.5",
"electron-builder": "^24.13.3",
"eslint": "^8.40.0",
"eslint-config-dmitmel": "github:dmitmel/eslint-config-dmitmel",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1",
"typescript": "^4.9.3"
"electron": "30.1.0",
"electron-builder": "25.0.0-alpha.9",
"eslint": "^9.4.0",
"eslint-plugin-n": "^17.8.1",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.1",
"typescript": "^5.4.5",
"typescript-eslint": "^7.12.0"
},
"dependencies": {
"arrpc": "github:OpenAsar/arrpc",
"cross-fetch": "^3.1.5",
"cross-fetch": "^4.0.0",
"electron-context-menu": "^4.0.0",
"extract-zip": "^2.0.1",
"v8-compile-cache": "^2.3.0",
"ws": "^8.11.0"
"v8-compile-cache": "^2.4.0",
"ws": "^8.17.0"
},
"build": {
"snap": {
@ -95,6 +95,6 @@
"applicationId": "smartfrigde.ArmCord"
}
},
"packageManager": "pnpm@9.1.1",
"packageManager": "pnpm@9.2.0",
"package-manager-strict": false
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
module.exports = {
/** @type {import("prettier").Config} */
const config = {
printWidth: 120,
tabWidth: 4,
useTabs: false,
@ -12,3 +13,5 @@ module.exports = {
arrowParens: "always",
endOfLine: "auto"
};
export default config;

View file

@ -1,6 +1,7 @@
import {app, dialog} from "electron";
import path from "path";
import fs from "fs";
import type {Settings} from "../types/settings.d.js";
import {getWindowStateLocation} from "./windowState.js";
export let firstRun: boolean;
export function checkForDataFolder(): void {
@ -11,64 +12,32 @@ export function checkForDataFolder(): void {
}
}
export interface Settings {
// Referenced for detecting a broken config.
"0"?: string;
// Referenced once for disabling mod updating.
noBundleUpdates?: boolean;
// Only used for external url warning dialog.
ignoreProtocolWarning?: boolean;
customIcon: string;
windowStyle: string;
channel: string;
armcordCSP: boolean;
minimizeToTray: boolean;
multiInstance: boolean;
spellcheck: boolean;
mods: string;
dynamicIcon: boolean;
mobileMode: boolean;
skipSplash: boolean;
performanceMode: string;
customJsBundle: RequestInfo | URL;
customCssBundle: RequestInfo | URL;
startMinimized: boolean;
useLegacyCapturer: boolean;
tray: boolean;
keybinds: Array<string>;
inviteWebsocket: boolean;
disableAutogain: boolean;
trayIcon: string;
doneSetup: boolean;
clientName: string;
}
export function getConfigLocation(): string {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}settings.json`;
}
export async function getConfig<K extends keyof Settings>(object: K): Promise<Settings[K]> {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata);
return returndata[object];
// REVIEW - If I remember correctly fs doesn't need async. I have adjusted the Promise<Settings[K]> to reflect so.
// Why touch it when it worked fine? The Async-ness of this function caused headaches in a lot of other places.
// Tested with src/tray.ts - Seems to work great!
// NOTE - Removed getConfigSync<K extends keyof Settings>(object: K) - Redundant now.
export function getConfig<K extends keyof Settings>(object: K): Settings[K] {
const rawData = fs.readFileSync(getConfigLocation(), "utf-8");
const returnData = JSON.parse(rawData) as Settings;
return returnData[object];
}
export function getConfigSync<K extends keyof Settings>(object: K) {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata);
return returndata[object];
}
export async function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): Promise<void> {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8");
let parsed = JSON.parse(rawdata);
export function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): void {
const rawData = fs.readFileSync(getConfigLocation(), "utf-8");
const parsed = JSON.parse(rawData) as Settings;
parsed[object] = toSet;
let toSave = JSON.stringify(parsed, null, 4);
const toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
}
export async function setConfigBulk(object: Settings): Promise<void> {
export function setConfigBulk(object: Settings): void {
let existingData = {};
try {
const existingDataBuffer = fs.readFileSync(getConfigLocation(), "utf-8");
existingData = JSON.parse(existingDataBuffer.toString());
existingData = JSON.parse(existingDataBuffer.toString()) as Settings;
} catch (error) {
// Ignore errors when the file doesn't exist or parsing fails
}
@ -78,7 +47,7 @@ export async function setConfigBulk(object: Settings): Promise<void> {
const toSave = JSON.stringify(mergedData, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
}
export async function checkIfConfigExists(): Promise<void> {
export function checkIfConfigExists(): void {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}settings.json`;
@ -91,7 +60,7 @@ export async function checkIfConfigExists(): Promise<void> {
console.log("First run of the ArmCord. Starting setup.");
setup();
firstRun = true;
} else if ((await getConfig("doneSetup")) == false) {
} else if (getConfig("doneSetup") == false) {
console.log("First run of the ArmCord. Starting setup.");
setup();
firstRun = true;
@ -99,9 +68,9 @@ export async function checkIfConfigExists(): Promise<void> {
console.log("ArmCord has been run before. Skipping setup.");
}
}
export async function checkIfConfigIsBroken(): Promise<void> {
export function checkIfConfigIsBroken(): void {
try {
let settingsData = fs.readFileSync(getConfigLocation(), "utf-8");
const settingsData = fs.readFileSync(getConfigLocation(), "utf-8");
JSON.parse(settingsData);
console.log("Config is fine");
} catch (e) {
@ -114,7 +83,7 @@ export async function checkIfConfigIsBroken(): Promise<void> {
);
}
try {
let windowData = fs.readFileSync(getWindowStateLocation(), "utf-8");
const windowData = fs.readFileSync(getWindowStateLocation(), "utf-8");
JSON.parse(windowData);
console.log("Window config is fine");
} catch (e) {

View file

@ -5,7 +5,7 @@ export function addStyle(styleString: string): void {
}
export function addScript(scriptString: string): void {
let script = document.createElement("script");
const script = document.createElement("script");
script.textContent = scriptString;
document.body.append(script);
}

View file

@ -2,7 +2,7 @@ import {app} from "electron";
import {getConfig} from "./config.js";
export let transparency: boolean;
export async function injectElectronFlags(): Promise<void> {
export function injectElectronFlags(): void {
// MIT License
// Copyright (c) 2022 GooseNest
@ -29,7 +29,7 @@ export async function injectElectronFlags(): Promise<void> {
battery: "--enable-features=TurnOffStreamingMediaCachingOnBattery --force_low_power_gpu", // Known to have better battery life for Chromium?
vaapi: "--ignore-gpu-blocklist --enable-features=VaapiVideoDecoder --enable-gpu-rasterization --enable-zero-copy --force_high_performance_gpu --use-gl=desktop --disable-features=UseChromeOSDirectVideoDecoder"
};
switch (await getConfig("performanceMode")) {
switch (getConfig("performanceMode")) {
case "performance":
console.log("Performance mode enabled");
app.commandLine.appendArgument(presets.performance);
@ -41,7 +41,7 @@ export async function injectElectronFlags(): Promise<void> {
default:
console.log("No performance modes set");
}
if ((await getConfig("windowStyle")) == "transparent" && process.platform === "win32") {
if (getConfig("windowStyle") == "transparent" && process.platform === "win32") {
transparency = true;
}
}

View file

@ -1,26 +1,27 @@
import {app} from "electron";
import path from "path";
import fs from "fs";
export async function setLang(language: string): Promise<void> {
import {i18nStrings} from "../types/i18nStrings";
export function setLang(language: string): void {
const langConfigFile = `${path.join(app.getPath("userData"), "/storage/")}lang.json`;
if (!fs.existsSync(langConfigFile)) {
fs.writeFileSync(langConfigFile, "{}", "utf-8");
}
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
parsed.lang = language;
let toSave = JSON.stringify(parsed, null, 4);
const toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(langConfigFile, toSave, "utf-8");
}
let language: string;
export async function getLang(object: string): Promise<string> {
export function getLang(object: string): string {
if (language == undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
language = parsed.lang;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");
@ -34,26 +35,26 @@ export async function getLang(object: string): Promise<string> {
if (!fs.existsSync(langPath)) {
langPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
}
let rawdata = fs.readFileSync(langPath, "utf-8");
let parsed = JSON.parse(rawdata);
let rawData = fs.readFileSync(langPath, "utf-8");
let parsed = JSON.parse(rawData) as i18nStrings;
if (parsed[object] == undefined) {
console.log(`${object} is undefined in ${language}`);
langPath = path.join(import.meta.dirname, "../", "/assets/lang/en-US.json");
rawdata = fs.readFileSync(langPath, "utf-8");
parsed = JSON.parse(rawdata);
rawData = fs.readFileSync(langPath, "utf-8");
parsed = JSON.parse(rawData) as i18nStrings;
return parsed[object];
} else {
return parsed[object];
}
}
export async function getLangName(): Promise<string> {
export function getLangName(): string {
if (language == undefined) {
try {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const langConfigFile = `${storagePath}lang.json`;
let rawdata = fs.readFileSync(langConfigFile, "utf-8");
let parsed = JSON.parse(rawdata);
const rawData = fs.readFileSync(langConfigFile, "utf-8");
const parsed = JSON.parse(rawData) as i18nStrings;
language = parsed.lang;
} catch (_e) {
console.log("Language config file doesn't exist. Fallback to English.");

View file

@ -1,34 +1,31 @@
import {app} from "electron";
import path from "path";
import fs from "fs";
export interface WindowState {
width: number;
height: number;
x: number;
y: number;
isMaximized: boolean;
}
import {WindowState} from "../types/windowState";
export function getWindowStateLocation() {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}window.json`;
}
export async function setWindowState(object: WindowState): Promise<void> {
export function setWindowState(object: WindowState): void {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const saveFile = `${storagePath}window.json`;
let toSave = JSON.stringify(object, null, 4);
const toSave = JSON.stringify(object, null, 4);
fs.writeFileSync(saveFile, toSave, "utf-8");
}
export async function getWindowState<K extends keyof WindowState>(object: K): Promise<WindowState[K]> {
// REVIEW - Similar to getConfig, this seems to return a promise when it has no async. Originally Promise<WindowState[K]>
export function getWindowState<K extends keyof WindowState>(object: K): WindowState[K] {
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}window.json`;
if (!fs.existsSync(settingsFile)) {
fs.writeFileSync(settingsFile, "{}", "utf-8");
}
let rawdata = fs.readFileSync(settingsFile, "utf-8");
let returndata = JSON.parse(rawdata);
console.log(`[Window state manager] ${returndata}`);
return returndata[object];
const rawData = fs.readFileSync(settingsFile, "utf-8");
const returnData = JSON.parse(rawData) as WindowState;
console.log(`[Window state manager] ${JSON.stringify(returnData)}`);
return returnData[object];
}

View file

@ -34,7 +34,9 @@
border-radius: 10px;
width: 80%;
height: 80%;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
box-shadow:
0 4px 8px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;

View file

@ -47,7 +47,9 @@
.desktop-capturer-selection__btn:hover,
.desktop-capturer-selection__btn:focus {
background: #7289da;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.45), 0 0 2px rgba(0, 0, 0, 0.25);
box-shadow:
0 0 4px rgba(0, 0, 0, 0.45),
0 0 2px rgba(0, 0, 0, 0.25);
color: #fff;
}
.desktop-capturer-selection__thumbnail {

View file

@ -50,7 +50,7 @@ function setConstraint(constraint, name, value) {
}
function disableAutogain(constraints) {
console.log("Automatically unsetting gain!", constraints);
if (constraints && constraints.audio) {
if (constraints?.audio) {
if (typeof constraints.audio !== "object") {
constraints.audio = {};
}

View file

@ -1,8 +1,8 @@
import electron from "electron";
import {getConfig} from "../../common/config.js";
const unstrictCSP = (): void => {
console.log("Setting up CSP unstricter...");
const unrestrictCSP = (): void => {
console.log("Setting up CSP unrestricter...");
electron.session.defaultSession.webRequest.onHeadersReceived(({responseHeaders, resourceType}, done) => {
if (!responseHeaders) return done({});
@ -19,9 +19,10 @@ const unstrictCSP = (): void => {
});
};
electron.app.whenReady().then(async () => {
if (await getConfig("armcordCSP")) {
unstrictCSP();
void electron.app.whenReady().then(() => {
// REVIEW - Awaiting the line above will hang the app.
if (getConfig("armcordCSP")) {
unrestrictCSP();
} else {
console.log("ArmCord CSP is disabled. The CSP should be managed by a third-party plugin(s).");
}

View file

@ -3,23 +3,23 @@ import extract from "extract-zip";
import path from "path";
import {getConfig} from "../../common/config.js";
import fs from "fs";
import {promisify} from "node:util";
import {pipeline} from "stream";
const streamPipeline = promisify(pipeline);
import {Readable} from "stream";
import type {ReadableStream} from "stream/web";
import {finished} from "stream/promises";
async function updateModBundle(): Promise<void> {
if ((await getConfig("noBundleUpdates")) == undefined ?? false) {
if (getConfig("noBundleUpdates") == undefined || false) {
try {
console.log("Downloading mod bundle");
const distFolder = `${app.getPath("userData")}/plugins/loader/dist/`;
while (!fs.existsSync(distFolder)) {
//waiting
}
let name: string = await getConfig("mods");
const name: string = getConfig("mods");
if (name == "custom") {
// aspy fix
let bundle: string = await (await fetch(await getConfig("customJsBundle"))).text();
const bundle: string = await (await fetch(getConfig("customJsBundle"))).text();
fs.writeFileSync(`${distFolder}bundle.js`, bundle, "utf-8");
let css: string = await (await fetch(await getConfig("customCssBundle"))).text();
const css: string = await (await fetch(getConfig("customCssBundle"))).text();
fs.writeFileSync(`${distFolder}bundle.css`, css, "utf-8");
} else {
const clientMods = {
@ -31,9 +31,9 @@ async function updateModBundle(): Promise<void> {
shelter: "https://armcord.app/placeholder.css"
};
console.log(clientMods[name as keyof typeof clientMods]);
let bundle: string = await (await fetch(clientMods[name as keyof typeof clientMods])).text();
const bundle: string = await (await fetch(clientMods[name as keyof typeof clientMods])).text();
fs.writeFileSync(`${distFolder}bundle.js`, bundle, "utf-8");
let css: string = await (await fetch(clientModsCss[name as keyof typeof clientModsCss])).text();
const css: string = await (await fetch(clientModsCss[name as keyof typeof clientModsCss])).text();
fs.writeFileSync(`${distFolder}bundle.css`, css, "utf-8");
}
} catch (e) {
@ -53,14 +53,14 @@ export let modInstallState: string;
export function updateModInstallState() {
modInstallState = "modDownload";
updateModBundle();
void updateModBundle(); // REVIEW - Awaiting this will hang the app on the splash
import("./plugin.js");
modInstallState = "done";
}
export async function installModLoader(): Promise<void> {
if ((await getConfig("mods")) == "none") {
if (getConfig("mods") == "none") {
modInstallState = "none";
fs.rmSync(`${app.getPath("userData")}/plugins/loader`, {recursive: true, force: true});
@ -80,7 +80,7 @@ export async function installModLoader(): Promise<void> {
fs.rmSync(`${app.getPath("userData")}/plugins/loader`, {recursive: true, force: true});
modInstallState = "installing";
let zipPath = `${app.getPath("temp")}/loader.zip`;
const zipPath = `${app.getPath("temp")}/loader.zip`;
if (!fs.existsSync(pluginFolder)) {
fs.mkdirSync(pluginFolder);
@ -88,31 +88,35 @@ export async function installModLoader(): Promise<void> {
}
// Add more of these later if needed!
let URLs = [
const URLs = [
"https://armcord.app/loader.zip",
"https://armcord.vercel.app/loader.zip",
"https://raw.githubusercontent.com/ArmCord/website/new/public/loader.zip"
];
let loaderZip: any;
// REVIEW - Rewrote this
while (true) {
if (URLs.length <= 0) throw new Error(`unexpected response ${loaderZip.statusText}`);
try {
loaderZip = await fetch(URLs[0]);
} catch (err) {
console.log("[Mod loader] Failed to download. Links left to try: " + (URLs.length - 1));
URLs.splice(0, 1);
continue;
let completed = false;
await fetch(URLs[0])
.then(async (loaderZip) => {
const fileStream = fs.createWriteStream(zipPath);
await finished(Readable.fromWeb(loaderZip.body as ReadableStream).pipe(fileStream)).then(
async () => {
await extract(zipPath, {dir: path.join(app.getPath("userData"), "plugins")}).then(() => {
updateModInstallState();
completed = true;
});
}
);
})
.catch(() => {
console.warn(`[Mod loader] Failed to download. Links left to try: ${URLs.length - 1}`);
URLs.splice(0, 1);
});
if (completed || URLs.length == 0) {
break;
}
await streamPipeline(loaderZip.body, fs.createWriteStream(zipPath));
await extract(zipPath, {dir: path.join(app.getPath("userData"), "plugins")});
updateModInstallState();
}
} catch (e) {
console.log("[Mod loader] Failed to install modloader");
console.error(e);

View file

@ -1,4 +1,4 @@
import * as fs from "fs";
import fs from "fs";
import {app, session} from "electron";
const userDataPath = app.getPath("userData");
const pluginFolder = `${userDataPath}/plugins`;
@ -6,12 +6,13 @@ if (!fs.existsSync(pluginFolder)) {
fs.mkdirSync(pluginFolder);
console.log("Created missing plugin folder");
}
app.whenReady().then(() => {
await app.whenReady().then(() => {
fs.readdirSync(pluginFolder).forEach((file) => {
try {
const manifest = fs.readFileSync(`${pluginFolder}/${file}/manifest.json`, "utf8");
const pluginFile = JSON.parse(manifest);
session.defaultSession.loadExtension(`${pluginFolder}/${file}`);
// NOTE - The below type assertion is just what we need from the chrome manifest
const pluginFile = JSON.parse(manifest) as {name: string; author: string};
void session.defaultSession.loadExtension(`${pluginFolder}/${file}`); // REVIEW - Awaiting this will cause plugins to not inject
console.log(`[Mod loader] Loaded ${pluginFile.name} made by ${pluginFile.author}`);
} catch (err) {
console.error(err);

View file

@ -1,19 +1,19 @@
//ipc stuff
import {app, clipboard, desktopCapturer, ipcMain, nativeImage, shell} from "electron";
import {app, clipboard, desktopCapturer, ipcMain, nativeImage, shell, SourcesOptions} from "electron";
import {mainWindow} from "./window.js";
import os from "os";
import fs from "fs";
import path from "path";
import {getConfig, setConfigBulk, getConfigLocation, Settings} from "../common/config.js";
import {getConfig, setConfigBulk, getConfigLocation} from "../common/config.js";
import {setLang, getLang, getLangName} from "../common/lang.js";
import {sleep} from "../common/sleep.js";
import {getVersion, getDisplayVersion} from "../common/version.js";
import {customTitlebar} from "../main.js";
import {createSettingsWindow} from "../settings/main.js";
import {splashWindow} from "../splash/main.js";
import {createTManagerWindow} from "../themeManager/main.js";
import {modInstallState} from "./extensions/mods.js";
import {Settings} from "../types/settings.d.js";
const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/");
@ -30,7 +30,7 @@ export function registerIpc(): void {
return getLang(toGet);
});
ipcMain.on("open-external-link", (_event, href: string) => {
shell.openExternal(href);
void shell.openExternal(href);
});
ipcMain.on("setPing", (_event, pingCount: number) => {
switch (os.platform()) {
@ -39,7 +39,7 @@ export function registerIpc(): void {
break;
case "win32":
if (pingCount > 0) {
let image = nativeImage.createFromPath(path.join(import.meta.dirname, "../", `/assets/ping.png`));
const image = nativeImage.createFromPath(path.join(import.meta.dirname, "../", `/assets/ping.png`));
mainWindow.setOverlayIcon(image, "badgeCount");
} else {
mainWindow.setOverlayIcon(null, "badgeCount");
@ -80,9 +80,9 @@ export function registerIpc(): void {
ipcMain.on("modInstallState", (event) => {
event.returnValue = modInstallState;
});
ipcMain.on("splashEnd", async () => {
ipcMain.on("splashEnd", () => {
splashWindow.close();
if (await getConfig("startMinimized")) {
if (getConfig("startMinimized")) {
mainWindow.hide();
} else {
mainWindow.show();
@ -92,75 +92,77 @@ export function registerIpc(): void {
app.relaunch();
app.exit();
});
ipcMain.on("minimizeToTray", async (event) => {
event.returnValue = await getConfig("minimizeToTray");
ipcMain.on("saveSettings", (_event, args: Settings) => {
setConfigBulk(args);
});
ipcMain.on("channel", async (event) => {
event.returnValue = await getConfig("channel");
ipcMain.on("minimizeToTray", (event) => {
event.returnValue = getConfig("minimizeToTray");
});
ipcMain.on("clientmod", async (event) => {
event.returnValue = await getConfig("mods");
ipcMain.on("channel", (event) => {
event.returnValue = getConfig("channel");
});
ipcMain.on("legacyCapturer", async (event) => {
event.returnValue = await getConfig("useLegacyCapturer");
ipcMain.on("clientmod", (event) => {
event.returnValue = getConfig("mods");
});
ipcMain.on("trayIcon", async (event) => {
event.returnValue = await getConfig("trayIcon");
ipcMain.on("legacyCapturer", (event) => {
event.returnValue = getConfig("useLegacyCapturer");
});
ipcMain.on("disableAutogain", async (event) => {
event.returnValue = await getConfig("disableAutogain");
ipcMain.on("trayIcon", (event) => {
event.returnValue = getConfig("trayIcon");
});
ipcMain.on("disableAutogain", (event) => {
event.returnValue = getConfig("disableAutogain");
});
ipcMain.on("titlebar", (event) => {
event.returnValue = customTitlebar;
});
ipcMain.on("mobileMode", async (event) => {
event.returnValue = await getConfig("mobileMode");
ipcMain.on("mobileMode", (event) => {
event.returnValue = getConfig("mobileMode");
});
// REVIEW - I don't see a reason to await the actual action of running the settings window. The user cannot open more than one anyway, as defined in the function.
ipcMain.on("openSettingsWindow", () => {
createSettingsWindow();
void createSettingsWindow();
});
ipcMain.on("openManagerWindow", () => {
createTManagerWindow();
void createTManagerWindow();
});
ipcMain.on("setting-armcordCSP", async (event) => {
if (await getConfig("armcordCSP")) {
ipcMain.on("setting-armcordCSP", (event) => {
if (getConfig("armcordCSP")) {
event.returnValue = true;
} else {
event.returnValue = false;
}
});
ipcMain.handle("DESKTOP_CAPTURER_GET_SOURCES", (_event, opts) => desktopCapturer.getSources(opts));
// NOTE - I assume this would return sources based on the fact that the function only ingests sources
ipcMain.handle("DESKTOP_CAPTURER_GET_SOURCES", (_event, opts: SourcesOptions) => desktopCapturer.getSources(opts));
ipcMain.on("saveSettings", (_event, args: Settings) => {
console.log(args);
setConfigBulk(args);
});
ipcMain.on("openStorageFolder", async () => {
// REVIEW - The lower 4 functions had await sleep(1000), I'm not sure why. Behavior is same regardless
ipcMain.on("openStorageFolder", () => {
shell.showItemInFolder(storagePath);
await sleep(1000);
});
ipcMain.on("openThemesFolder", async () => {
ipcMain.on("openThemesFolder", () => {
shell.showItemInFolder(themesPath);
await sleep(1000);
});
ipcMain.on("openPluginsFolder", async () => {
ipcMain.on("openPluginsFolder", () => {
shell.showItemInFolder(pluginsPath);
await sleep(1000);
});
ipcMain.on("openCrashesFolder", async () => {
ipcMain.on("openCrashesFolder", () => {
shell.showItemInFolder(path.join(app.getPath("temp"), `${app.getName()} Crashes`));
await sleep(1000);
});
ipcMain.on("getLangName", async (event) => {
event.returnValue = await getLangName();
ipcMain.on("getLangName", (event) => {
event.returnValue = getLangName();
});
ipcMain.on("crash", async () => {
ipcMain.on("crash", () => {
process.crash();
});
ipcMain.handle("getSetting", (_event, toGet: keyof Settings) => {
return getConfig(toGet);
});
ipcMain.on("copyDebugInfo", () => {
let settingsFileContent = fs.readFileSync(getConfigLocation(), "utf-8");
const settingsFileContent = fs.readFileSync(getConfigLocation(), "utf-8");
clipboard.writeText(
`**OS:** ${os.platform()} ${os.version()}\n**Architecture:** ${os.arch()}\n**ArmCord version:** ${getVersion()}\n**Electron version:** ${
process.versions.electron

View file

@ -1,17 +1,9 @@
import {BrowserWindow, Menu, app, clipboard} from "electron";
import {BrowserWindow, Menu, app} from "electron";
import {mainWindow} from "./window.js";
import {createSettingsWindow} from "../settings/main.js";
function paste(contents: any): void {
const contentTypes = clipboard.availableFormats().toString();
//Workaround: fix pasting the images.
if (contentTypes.includes("image/") && contentTypes.includes("text/html")) {
clipboard.writeImage(clipboard.readImage());
}
contents.paste();
}
export async function setMenu(): Promise<void> {
let template: Electron.MenuItemConstructorOptions[] = [
export function setMenu(): void {
const template: Electron.MenuItemConstructorOptions[] = [
{
label: "ArmCord",
submenu: [
@ -28,7 +20,7 @@ export async function setMenu(): Promise<void> {
label: "Open settings",
accelerator: "CmdOrCtrl+Shift+'",
click() {
createSettingsWindow();
void createSettingsWindow();
}
},
{
@ -67,13 +59,6 @@ export async function setMenu(): Promise<void> {
{type: "separator"},
{label: "Cut", accelerator: "CmdOrCtrl+X", role: "cut"},
{label: "Copy", accelerator: "CmdOrCtrl+C", role: "copy"},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
click() {
paste(mainWindow.webContents);
}
},
{label: "Select All", accelerator: "CmdOrCtrl+A", role: "selectAll"}
]
},

View file

@ -1,8 +1,9 @@
import {contextBridge, ipcRenderer} from "electron";
import {contextBridge, ipcRenderer, type SourcesOptions} from "electron";
import {injectTitlebar} from "./titlebar.mjs";
import type {ArmCordWindow} from "../../types/armcordWindow.d.js";
const CANCEL_ID = "desktop-capturer-selection__cancel";
const desktopCapturer = {
getSources: (opts: any) => ipcRenderer.invoke("DESKTOP_CAPTURER_GET_SOURCES", opts)
getSources: (opts: SourcesOptions) => ipcRenderer.invoke("DESKTOP_CAPTURER_GET_SOURCES", opts)
};
interface IPCSources {
id: string;
@ -10,9 +11,9 @@ interface IPCSources {
thumbnail: HTMLCanvasElement;
}
async function getDisplayMediaSelector(): Promise<string> {
const sources: IPCSources[] = await desktopCapturer.getSources({
const sources = (await desktopCapturer.getSources({
types: ["screen", "window"]
});
})) as IPCSources[];
return `<div class="desktop-capturer-selection__scroller">
<ul class="desktop-capturer-selection__list">
${sources
@ -44,24 +45,25 @@ contextBridge.exposeInMainWorld("armcord", {
},
titlebar: {
injectTitlebar: () => injectTitlebar(),
isTitlebar: ipcRenderer.sendSync("titlebar")
isTitlebar: ipcRenderer.sendSync("titlebar") as boolean
},
electron: process.versions.electron,
channel: ipcRenderer.sendSync("channel"),
channel: ipcRenderer.sendSync("channel") as string,
setPingCount: (pingCount: number) => ipcRenderer.send("setPing", pingCount),
setTrayIcon: (favicon: string) => ipcRenderer.send("sendTrayIcon", favicon),
getLang: (toGet: string) =>
ipcRenderer.invoke("getLang", toGet).then((result) => {
return result;
return result as string;
}),
getDisplayMediaSelector,
version: ipcRenderer.sendSync("get-app-version", "app-version"),
mods: ipcRenderer.sendSync("clientmod"),
version: ipcRenderer.sendSync("get-app-version", "app-version") as string,
mods: ipcRenderer.sendSync("clientmod") as string,
openSettingsWindow: () => ipcRenderer.send("openSettingsWindow")
});
} as ArmCordWindow);
let windowCallback: (arg0: object) => void;
contextBridge.exposeInMainWorld("ArmCordRPC", {
listen: (callback: any) => {
// REVIEW - I don't think this is right
listen: (callback: () => void) => {
windowCallback = callback;
}
});

View file

@ -1,6 +1,6 @@
import {addStyle} from "../../common/dom.js";
import * as fs from "fs";
import * as path from "path";
import fs from "fs";
import path from "path";
export function injectMobileStuff(): void {
document.addEventListener("DOMContentLoaded", function () {
const mobileCSS = path.join(import.meta.dirname, "../", "/content/css/mobile.css");

View file

@ -1,9 +0,0 @@
const optimize = (orig: Function) =>
function (this: any, ...args: any[]) {
if (typeof args[0]?.className === "string" && args[0].className.indexOf("activity") !== -1)
return setTimeout(() => orig.apply(this, args), 100);
return orig.apply(this, args);
};
Element.prototype.removeChild = optimize(Element.prototype.removeChild);

View file

@ -0,0 +1,18 @@
type OptimizableFunction<T extends Node> = (child: T) => T;
const optimize = <T extends Node>(orig: OptimizableFunction<T>) => {
return function (this: Element, ...args: [Element]) {
if (typeof args[0]?.className === "string" && args[0].className.indexOf("activity") !== -1) {
// @ts-expect-error - // FIXME
return setTimeout(() => orig.apply(this, args), 100);
}
// @ts-expect-error - // FIXME
return orig.apply(this, args);
} as unknown as OptimizableFunction<T>;
};
// We are taking in the function itself
// eslint-disable-next-line @typescript-eslint/unbound-method
Element.prototype.removeChild = optimize(Element.prototype.removeChild);
// Thanks Ari - <@1249446413952225452>

View file

@ -1,36 +1,39 @@
import "./bridge.mjs";
import "./optimizer.mjs";
import "./settings.mjs";
import "./bridge.js";
import "./optimizer.js";
import "./settings.js";
import {ipcRenderer} from "electron";
import * as fs from "fs";
import * as path from "path";
import {injectMobileStuff} from "./mobile.mjs";
import fs from "fs";
import path from "path";
import {injectMobileStuff} from "./mobile.js";
import {fixTitlebar, injectTitlebar} from "./titlebar.mjs";
import {injectSettings} from "./settings.mjs";
import {injectSettings} from "./settings.js";
import {addStyle, addScript} from "../../common/dom.js";
import {sleep} from "../../common/sleep.js";
import type {ArmCordWindow} from "../../types/armcordWindow.d.js";
window.localStorage.setItem("hideNag", "true");
if (ipcRenderer.sendSync("legacyCapturer")) {
console.warn("Using legacy capturer module");
import("./capturer.mjs");
import("./capturer.js");
}
const version = ipcRenderer.sendSync("displayVersion");
async function updateLang(): Promise<void> {
const version = ipcRenderer.sendSync("displayVersion") as string;
function updateLang(): void {
const value = `; ${document.cookie}`;
const parts: any = value.split(`; locale=`);
if (parts.length === 2) ipcRenderer.send("setLang", parts.pop().split(";").shift());
const parts = value.split(`; locale=`);
if (parts.length === 2) ipcRenderer.send("setLang", parts.pop()?.split(";").shift());
}
declare global {
interface Window {
armcord: any;
// REVIEW - Assumption, this was previously any
armcord: ArmCordWindow;
}
}
console.log(`ArmCord ${version}`);
ipcRenderer.on("themeLoader", (_event, message) => {
ipcRenderer.on("themeLoader", (_event, message: string) => {
addStyle(message);
});
@ -40,7 +43,7 @@ if (ipcRenderer.sendSync("titlebar")) {
if (ipcRenderer.sendSync("mobileMode")) {
injectMobileStuff();
}
sleep(5000).then(async () => {
await sleep(5000).then(() => {
// dirty hack to make clicking notifications focus ArmCord
addScript(`
(() => {
@ -62,7 +65,7 @@ sleep(5000).then(async () => {
addScript(fs.readFileSync(path.join(import.meta.dirname, "../", "/content/js/rpc.js"), "utf8"));
const cssPath = path.join(import.meta.dirname, "../", "/content/css/discord.css");
addStyle(fs.readFileSync(cssPath, "utf8"));
await updateLang();
updateLang();
});
// Settings info version injection

View file

@ -1,9 +1,8 @@
import * as path from "path";
import * as fs from "fs";
import path from "path";
import fs from "fs";
import {addStyle} from "../../common/dom.js";
import {WebviewTag} from "electron";
var webview = `<webview src="${path.join(
const webview = `<webview src="${path.join(
"file://",
import.meta.dirname,
"../",
@ -25,8 +24,8 @@ export function injectSettings() {
document.addEventListener("DOMContentLoaded", function (_event) {
const settingsCssPath = path.join(import.meta.dirname, "../", "/content/css/inAppSettings.css");
addStyle(fs.readFileSync(settingsCssPath, "utf8"));
const webview = document.querySelector("webview") as WebviewTag;
const webview = document.querySelector("webview")!;
webview.addEventListener("console-message", (e) => {
console.log("Settings page logged a message:", e.message);
console.log("Settings page logged a message:", e as Electron.ConsoleMessageEvent);
});
});

View file

@ -1,7 +1,7 @@
import {ipcRenderer} from "electron";
import {addStyle} from "../../common/dom.js";
import * as fs from "fs";
import * as path from "path";
import fs from "fs";
import path from "path";
import os from "os";
export function injectTitlebar(): void {
document.addEventListener("DOMContentLoaded", function (_event) {

View file

@ -12,7 +12,7 @@ function showAudioDialog(): boolean {
detail: "Selecting yes will make viewers of your stream hear your entire system audio."
};
dialog.showMessageBox(capturerWindow, options).then(({response}) => {
void dialog.showMessageBox(capturerWindow, options).then(({response}) => {
if (response == 0) {
return true;
} else {
@ -23,15 +23,17 @@ function showAudioDialog(): boolean {
}
function registerCustomHandler(): void {
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
console.log(request);
const sources = await desktopCapturer.getSources({
void desktopCapturer
.getSources({
types: ["screen", "window"]
});
})
.then((sources) => {
console.log(sources);
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") {
console.log("WebRTC Capturer detected, skipping window creation."); //assume webrtc capturer is used
var options: Electron.Streams = {video: sources[0]};
let options: Electron.Streams = {video: sources[0]};
if (showAudioDialog() == true) options = {video: sources[0], audio: "loopbackWithMute"};
callback(options);
} else {
@ -46,25 +48,26 @@ function registerCustomHandler(): void {
webPreferences: {
sandbox: false,
spellcheck: false,
preload: path.join(import.meta.dirname, "preload.js")
preload: path.join(import.meta.dirname, "preload.mjs")
}
});
ipcMain.once("selectScreenshareSource", (_event, id, name) => {
ipcMain.once("selectScreenshareSource", (_event, id: string, name: string) => {
//console.log(sources[id]);
//console.log(id);
capturerWindow.close();
let result = {id, name};
const result = {id, name};
if (process.platform === "linux" || process.platform === "win32") {
var options: Electron.Streams = {video: sources[0]};
let options: Electron.Streams = {video: sources[0]};
if (showAudioDialog() == true) options = {video: sources[0], audio: "loopbackWithMute"};
callback(options);
} else {
callback({video: result});
}
});
capturerWindow.loadURL(`file://${import.meta.dirname}/picker.html`);
void capturerWindow.loadURL(`file://${import.meta.dirname}/picker.html`);
capturerWindow.webContents.send("getSources", sources);
}
});
});
}
registerCustomHandler();

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View file

@ -4,9 +4,9 @@ interface IPCSources {
name: string;
thumbnail: HTMLCanvasElement;
}
async function addDisplays(): Promise<void> {
ipcRenderer.once("getSources", (_event, arg) => {
let sources: IPCSources[] = arg;
function addDisplays(): void {
ipcRenderer.once("getSources", (_event, arg: IPCSources[]) => {
const sources = arg;
console.log(sources);
const selectionElem = document.createElement("div");
selectionElem.classList.add("desktop-capturer-selection");
@ -33,7 +33,7 @@ async function addDisplays(): Promise<void> {
</div>`;
document.body.appendChild(selectionElem);
document.querySelectorAll(".desktop-capturer-selection__btn").forEach((button) => {
button.addEventListener("click", async () => {
button.addEventListener("click", () => {
try {
const id = button.getAttribute("data-id");
const title = button.getAttribute("title");

View file

@ -1,9 +1,11 @@
// To allow seamless switching between custom titlebar and native os titlebar,
// I had to add most of the window creation code here to split both into seperete functions
// I had to add most of the window creation code here to split both into separate functions
// WHY? Because I can't use the same code for both due to annoying bug with value `frame` not responding to variables
// I'm sorry for this mess but I'm not sure how to fix it.
import {BrowserWindow, MessageBoxOptions, app, dialog, nativeImage, shell} from "electron";
import path from "path";
import type EventEmitter from "events";
import {ThemeManifest} from "../types/themeManifest.d.js";
import {registerIpc} from "./ipc.js";
import {setMenu} from "./menu.js";
import * as fs from "fs";
@ -28,7 +30,7 @@ contextMenu({
// Only show it when right-clicking text
visible: parameters.selectionText.trim().length > 0,
click: () => {
shell.openExternal(`https://google.com/search?q=${encodeURIComponent(parameters.selectionText)}`);
void shell.openExternal(`https://google.com/search?q=${encodeURIComponent(parameters.selectionText)}`);
}
},
{
@ -36,27 +38,30 @@ contextMenu({
// Only show it when right-clicking text
visible: parameters.selectionText.trim().length > 0,
click: () => {
shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(parameters.selectionText)}`);
void shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(parameters.selectionText)}`);
}
}
]
});
async function doAfterDefiningTheWindow(): Promise<void> {
if ((await getWindowState("isMaximized")) ?? false) {
function doAfterDefiningTheWindow(): void {
if (getWindowState("isMaximized") ?? false) {
mainWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen
mainWindow.maximize();
mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
mainWindow.hide(); // please don't flashbang the user
}
if ((await getConfig("windowStyle")) == "transparency" && process.platform === "win32") {
if (getConfig("windowStyle") == "transparency" && process.platform === "win32") {
mainWindow.setBackgroundMaterial("mica");
if ((await getConfig("startMinimized")) == false) {
if (getConfig("startMinimized") == false) {
mainWindow.show();
}
}
let ignoreProtocolWarning = await getConfig("ignoreProtocolWarning");
// REVIEW - Test the protocol warning. I was not sure how to get it to pop up. For now I've voided the promises.
const ignoreProtocolWarning = getConfig("ignoreProtocolWarning");
registerIpc();
if (await getConfig("mobileMode")) {
if (getConfig("mobileMode")) {
mainWindow.webContents.userAgent =
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.149 Mobile Safari/537.36";
} else {
@ -96,9 +101,9 @@ async function doAfterDefiningTheWindow(): Promise<void> {
}
};
if (url.startsWith("https:") || url.startsWith("http:") || url.startsWith("mailto:")) {
shell.openExternal(url);
void shell.openExternal(url);
} else if (ignoreProtocolWarning) {
shell.openExternal(url);
void shell.openExternal(url);
} else {
const options: MessageBoxOptions = {
type: "question",
@ -111,7 +116,7 @@ async function doAfterDefiningTheWindow(): Promise<void> {
checkboxChecked: false
};
dialog.showMessageBox(mainWindow, options).then(({response, checkboxChecked}) => {
void dialog.showMessageBox(mainWindow, options).then(({response, checkboxChecked}) => {
console.log(response, checkboxChecked);
if (checkboxChecked) {
if (response == 0) {
@ -121,13 +126,13 @@ async function doAfterDefiningTheWindow(): Promise<void> {
}
}
if (response == 0) {
shell.openExternal(url);
void shell.openExternal(url);
}
});
}
return {action: "deny"};
});
if ((await getConfig("useLegacyCapturer")) == false) {
if (getConfig("useLegacyCapturer") == false) {
console.log("Starting screenshare module...");
import("./screenshare/main.js");
}
@ -137,9 +142,12 @@ async function doAfterDefiningTheWindow(): Promise<void> {
(_, callback) => callback({cancel: true})
);
if ((await getConfig("trayIcon")) == "default" || (await getConfig("dynamicIcon"))) {
mainWindow.webContents.on("page-favicon-updated", async () => {
let faviconBase64 = await mainWindow.webContents.executeJavaScript(`
if (getConfig("trayIcon") == "default" || getConfig("dynamicIcon")) {
mainWindow.webContents.on("page-favicon-updated", () => {
// REVIEW - no need to await if we just .then() - This works!
void mainWindow.webContents
.executeJavaScript(
`
var getFavicon = function(){
var favicon = undefined;
var nodeList = document.getElementsByTagName("link");
@ -153,29 +161,33 @@ async function doAfterDefiningTheWindow(): Promise<void> {
return favicon;
}
getFavicon()
`);
let buf = Buffer.from(faviconBase64.replace(/^data:image\/\w+;base64,/, ""), "base64");
`
)
.then((faviconBase64: string) => {
const buf = Buffer.from(faviconBase64.replace(/^data:image\/\w+;base64,/, ""), "base64");
fs.writeFileSync(path.join(app.getPath("temp"), "/", "tray.png"), buf, "utf-8");
let trayPath = nativeImage.createFromPath(path.join(app.getPath("temp"), "/", "tray.png"));
if (process.platform === "darwin" && trayPath.getSize().height > 22)
trayPath = trayPath.resize({height: 22});
if (process.platform === "win32" && trayPath.getSize().height > 32)
trayPath = trayPath.resize({height: 32});
if (await getConfig("tray")) {
if ((await getConfig("trayIcon")) == "default") {
if (getConfig("tray")) {
if (getConfig("trayIcon") == "default") {
tray.setImage(trayPath);
}
}
if (await getConfig("dynamicIcon")) {
if (getConfig("dynamicIcon")) {
mainWindow.setIcon(trayPath);
}
});
});
}
mainWindow.webContents.on("page-title-updated", async (e, title) => {
mainWindow.webContents.on("page-title-updated", (e, title) => {
const armCordSuffix = " - ArmCord"; /* identify */
if (!title.endsWith(armCordSuffix)) {
e.preventDefault();
await mainWindow.webContents.executeJavaScript(
// REVIEW - I don't see a reason to wait for the titlebar to update
void mainWindow.webContents.executeJavaScript(
`document.title = '${title.replace("Discord |", "") + armCordSuffix}'`
);
}
@ -193,7 +205,7 @@ async function doAfterDefiningTheWindow(): Promise<void> {
fs.readdirSync(themesFolder).forEach((file) => {
try {
const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8");
let themeFile = JSON.parse(manifest);
const themeFile = JSON.parse(manifest) as ThemeManifest;
if (
fs
.readFileSync(path.join(userDataPath, "/disabled.txt"))
@ -213,23 +225,23 @@ async function doAfterDefiningTheWindow(): Promise<void> {
}
});
});
await setMenu();
mainWindow.on("close", async (e) => {
setMenu();
mainWindow.on("close", (e) => {
if (process.platform === "darwin" && forceQuit) {
mainWindow.close();
} else {
let [width, height] = mainWindow.getSize();
await setWindowState({
const [width, height] = mainWindow.getSize();
setWindowState({
width,
height,
isMaximized: mainWindow.isMaximized(),
x: mainWindow.getPosition()[0],
y: mainWindow.getPosition()[1]
});
if (await getConfig("minimizeToTray")) {
if (getConfig("minimizeToTray")) {
e.preventDefault();
mainWindow.hide();
} else if (!(await getConfig("minimizeToTray"))) {
} else if (!getConfig("minimizeToTray")) {
e.preventDefault();
app.quit();
}
@ -244,43 +256,49 @@ async function doAfterDefiningTheWindow(): Promise<void> {
}
});
}
// REVIEW - Awaiting javascript execution is silly
mainWindow.on("focus", () => {
mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`);
void mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`);
});
mainWindow.on("blur", () => {
mainWindow.webContents.executeJavaScript(`document.body.setAttribute("unFocused", "");`);
void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("unFocused", "");`);
});
mainWindow.on("maximize", () => {
mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
});
mainWindow.on("unmaximize", () => {
mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`);
void mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`);
});
if ((await getConfig("inviteWebsocket")) == true) {
const server = await new RPCServer();
if (getConfig("inviteWebsocket")) {
// NOTE - RPCServer appears to be untyped. cool.
// REVIEW - Whatever Ducko has done here to make an async constructor is awful.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
new RPCServer().then((server: EventEmitter) => {
server.on("activity", (data: string) => mainWindow.webContents.send("rpc", data));
server.on("invite", (code: string) => {
console.log(code);
createInviteWindow(code);
});
});
}
if (firstRun) {
mainWindow.close();
}
//loadURL broke for no good reason after E28
mainWindow.loadFile(`${import.meta.dirname}/../splash/redirect.html`);
void mainWindow.loadFile(`${import.meta.dirname}/../splash/redirect.html`);
if (await getConfig("skipSplash")) {
if (getConfig("skipSplash")) {
mainWindow.show();
}
}
export async function createCustomWindow(): Promise<void> {
export function createCustomWindow(): void {
mainWindow = new BrowserWindow({
width: (await getWindowState("width")) ?? 835,
height: (await getWindowState("height")) ?? 600,
x: await getWindowState("x"),
y: await getWindowState("y"),
width: getWindowState("width") ?? 835,
height: getWindowState("height") ?? 600,
x: getWindowState("x"),
y: getWindowState("y"),
title: "ArmCord",
show: false,
darkTheme: true,
@ -292,17 +310,17 @@ export async function createCustomWindow(): Promise<void> {
webviewTag: true,
sandbox: false,
preload: path.join(import.meta.dirname, "preload/preload.mjs"),
spellcheck: await getConfig("spellcheck")
spellcheck: getConfig("spellcheck")
}
});
doAfterDefiningTheWindow();
}
export async function createNativeWindow(): Promise<void> {
export function createNativeWindow(): void {
mainWindow = new BrowserWindow({
width: (await getWindowState("width")) ?? 835,
height: (await getWindowState("height")) ?? 600,
x: await getWindowState("x"),
y: await getWindowState("y"),
width: getWindowState("width") ?? 835,
height: getWindowState("height") ?? 600,
x: getWindowState("x"),
y: getWindowState("y"),
title: "ArmCord",
darkTheme: true,
icon: iconPath,
@ -314,17 +332,17 @@ export async function createNativeWindow(): Promise<void> {
webviewTag: true,
sandbox: false,
preload: path.join(import.meta.dirname, "preload/preload.mjs"),
spellcheck: await getConfig("spellcheck")
spellcheck: getConfig("spellcheck")
}
});
doAfterDefiningTheWindow();
}
export async function createTransparentWindow(): Promise<void> {
export function createTransparentWindow(): void {
mainWindow = new BrowserWindow({
width: (await getWindowState("width")) ?? 835,
height: (await getWindowState("height")) ?? 600,
x: await getWindowState("x"),
y: await getWindowState("y"),
width: getWindowState("width") ?? 835,
height: getWindowState("height") ?? 600,
x: getWindowState("x"),
y: getWindowState("y"),
title: "ArmCord",
darkTheme: true,
icon: iconPath,
@ -336,12 +354,12 @@ export async function createTransparentWindow(): Promise<void> {
sandbox: false,
webviewTag: true,
preload: path.join(import.meta.dirname, "preload/preload.mjs"),
spellcheck: await getConfig("spellcheck")
spellcheck: getConfig("spellcheck")
}
});
doAfterDefiningTheWindow();
}
export async function createInviteWindow(code: string): Promise<void> {
export function createInviteWindow(code: string): void {
inviteWindow = new BrowserWindow({
width: 800,
height: 600,
@ -352,15 +370,16 @@ export async function createInviteWindow(code: string): Promise<void> {
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
spellcheck: await getConfig("spellcheck")
spellcheck: getConfig("spellcheck")
}
});
let formInviteURL = `https://discord.com/invite/${code}`;
const formInviteURL = `https://discord.com/invite/${code}`;
inviteWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
if (details.url.includes("ws://")) return callback({cancel: true});
return callback({});
});
inviteWindow.loadURL(formInviteURL);
// REVIEW - This shouldn't matter, since below we have an event on it
void inviteWindow.loadURL(formInviteURL);
inviteWindow.webContents.once("did-finish-load", () => {
if (!mainWindow.webContents.isLoading()) {
inviteWindow.show();

View file

@ -11,23 +11,22 @@ import {createSplashWindow} from "./splash/main.js";
import {createSetupWindow} from "./setup/main.js";
import {
setConfig,
getConfigSync,
checkForDataFolder,
checkIfConfigExists,
checkIfConfigIsBroken,
getConfig,
firstRun,
Settings,
getConfigLocation
} from "./common/config.js";
import {injectElectronFlags} from "./common/flags.js";
import {setLang} from "./common/lang.js";
import {installModLoader} from "./discord/extensions/mods.js";
export let iconPath: string;
export let settings: any;
import type {Settings} from "./types/settings";
export let settings: Settings;
export let customTitlebar: boolean;
app.on("render-process-gone", (event, webContents, details) => {
app.on("render-process-gone", (_event, _webContents, details) => {
if (details.reason == "crashed") {
app.relaunch();
}
@ -35,23 +34,23 @@ app.on("render-process-gone", (event, webContents, details) => {
async function args(): Promise<void> {
let argNum = 2;
if (process.argv[0] == "electron") argNum++;
let args = process.argv[argNum];
const args = process.argv[argNum];
if (args == undefined) return;
if (args.startsWith("--")) return; //electron flag
if (args.includes("=")) {
let e = args.split("=");
await setConfig(e[0] as keyof Settings, e[1]);
const e = args.split("=");
setConfig(e[0] as keyof Settings, e[1]);
console.log(`Setting ${e[0]} to ${e[1]}`);
app.relaunch();
app.exit();
} else if (args == "themes") {
app.whenReady().then(async () => {
createTManagerWindow();
await app.whenReady().then(async () => {
await createTManagerWindow();
});
}
}
args(); // i want my top level awaits
if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false ?? undefined)) {
await args(); // i want my top level awaits - IMPLEMENTED :)
if (!app.requestSingleInstanceLock() && getConfig("multiInstance") == (false ?? undefined)) {
// if value isn't set after 3.2.4
// kill if 2nd instance
app.quit();
@ -82,21 +81,22 @@ if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false
checkIfConfigIsBroken();
injectElectronFlags();
console.log("[Config Manager] Current config: " + fs.readFileSync(getConfigLocation(), "utf-8"));
app.whenReady().then(async () => {
if ((await getConfig("customIcon")) !== undefined ?? null) {
iconPath = await getConfig("customIcon");
void app.whenReady().then(async () => {
// REVIEW - Awaiting the line above will cause a hang at startup
if (getConfig("customIcon") !== null) {
iconPath = getConfig("customIcon");
} else {
iconPath = path.join(import.meta.dirname, "../", "/assets/desktop.png");
}
async function init(): Promise<void> {
if ((await getConfig("skipSplash")) == false) {
createSplashWindow();
if (getConfig("skipSplash") == false) {
void createSplashWindow(); // REVIEW - Awaiting will hang at start
}
if (firstRun == true) {
await setLang(new Intl.DateTimeFormat().resolvedOptions().locale);
createSetupWindow();
setLang(new Intl.DateTimeFormat().resolvedOptions().locale);
await createSetupWindow();
}
switch (await getConfig("windowStyle")) {
switch (getConfig("windowStyle")) {
case "default":
createCustomWindow();
customTitlebar = true;
@ -125,8 +125,9 @@ if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false
callback(true);
}
});
app.on("activate", async function () {
if (BrowserWindow.getAllWindows().length === 0) await init();
app.on("activate", function () {
// REVIEW - I don't think it really matters if this promise is voided
if (BrowserWindow.getAllWindows().length === 0) void init();
});
});
}

View file

@ -2,10 +2,11 @@ import {BrowserWindow, app, shell} from "electron";
import path from "path";
import fs from "fs";
import {getDisplayVersion} from "../common/version.js";
import type {ThemeManifest} from "../types/themeManifest.d.js";
let settingsWindow: BrowserWindow;
let instance = 0;
export function createSettingsWindow(): void {
export async function createSettingsWindow(): Promise<void> {
console.log("Creating a settings window.");
instance += 1;
if (instance > 1) {
@ -28,7 +29,7 @@ export function createSettingsWindow(): void {
}
});
async function settingsLoadPage(): Promise<void> {
settingsWindow.loadURL(`file://${import.meta.dirname}/settings.html`);
await settingsWindow.loadURL(`file://${import.meta.dirname}/settings.html`);
}
const userDataPath = app.getPath("userData");
const themesFolder = `${userDataPath}/themes/`;
@ -43,7 +44,7 @@ export function createSettingsWindow(): void {
fs.readdirSync(themesFolder).forEach((file) => {
try {
const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8");
let themeFile = JSON.parse(manifest);
const themeFile = JSON.parse(manifest) as ThemeManifest;
if (
fs
.readFileSync(path.join(userDataPath, "/disabled.txt"))
@ -64,10 +65,10 @@ export function createSettingsWindow(): void {
});
});
settingsWindow.webContents.setWindowOpenHandler(({url}) => {
shell.openExternal(url);
void shell.openExternal(url);
return {action: "deny"};
});
settingsLoadPage();
await settingsLoadPage();
settingsWindow.on("close", () => {
instance = 0;
});

View file

@ -1,11 +1,14 @@
import {contextBridge, ipcRenderer} from "electron";
import {Settings} from "../types/settings";
//import {addStyle} from "../utils.js";
console.log("ArmCord Settings");
console.log(process.platform);
contextBridge.exposeInMainWorld("settings", {
save: (...args: any) => ipcRenderer.send("saveSettings", ...args),
// REVIEW - this may be typed incorrectly, I'm not sure how "..." works
save: (...args: Settings[]) => ipcRenderer.send("saveSettings", ...args),
restart: () => ipcRenderer.send("restart"),
saveAlert: (restartFunc: any) => ipcRenderer.send("saveAlert", restartFunc),
// REVIEW - I couldn't find a reference to anything about the below function
saveAlert: (restartFunc: () => void) => ipcRenderer.send("saveAlert", restartFunc),
getLang: (toGet: string) => ipcRenderer.invoke("getLang", toGet),
get: (toGet: string) => ipcRenderer.invoke("getSetting", toGet),
openThemesFolder: () => ipcRenderer.send("openThemesFolder"),
@ -17,7 +20,8 @@ contextBridge.exposeInMainWorld("settings", {
crash: () => ipcRenderer.send("crash"),
os: process.platform
});
/*
ipcRenderer.on("themeLoader", (_event, message) => {
//addStyle(message);
});
*/

View file

@ -260,6 +260,7 @@ select option {
select {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
background-color: var(--background-secondary-alt);
background-position: center right;
background-repeat: no-repeat;

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View file

@ -1,11 +1,14 @@
import {BrowserWindow, app, ipcMain} from "electron";
import path from "path";
import * as fs from "fs";
import fs from "fs";
import {iconPath} from "../main.js";
import {setConfigBulk, getConfigLocation, Settings} from "../common/config.js";
import {setConfigBulk, getConfigLocation} from "../common/config.js";
import type {Settings} from "../types/settings.d.js";
let setupWindow: BrowserWindow;
export function createSetupWindow(): void {
export async function createSetupWindow(): Promise<void> {
// NOTE - intentionally hang the process until setup is completed
return new Promise((resolve) => {
setupWindow = new BrowserWindow({
width: 390,
height: 470,
@ -23,6 +26,7 @@ export function createSetupWindow(): void {
ipcMain.on("saveSettings", (_event, args: Settings) => {
console.log(args);
setConfigBulk(args);
resolve();
});
ipcMain.on("setup-minimize", () => {
setupWindow.minimize();
@ -30,13 +34,14 @@ export function createSetupWindow(): void {
ipcMain.on("setup-getOS", (event) => {
event.returnValue = process.platform;
});
ipcMain.on("setup-quit", async () => {
fs.unlink(await getConfigLocation(), (err) => {
ipcMain.on("setup-quit", () => {
fs.unlink(getConfigLocation(), (err) => {
if (err) throw err;
console.log('Closed during setup. "settings.json" was deleted');
app.quit();
});
});
setupWindow.loadURL(`file://${import.meta.dirname}/setup.html`);
void setupWindow.loadURL(`file://${import.meta.dirname}/setup.html`);
});
}

View file

@ -1,13 +1,14 @@
import {contextBridge, ipcRenderer} from "electron";
import {injectTitlebar} from "../discord/preload/titlebar.mjs";
import {Settings} from "../types/settings";
injectTitlebar();
contextBridge.exposeInMainWorld("armcordinternal", {
restart: () => ipcRenderer.send("restart"),
getOS: ipcRenderer.sendSync("setup-getOS"),
saveSettings: (...args: any) => ipcRenderer.send("saveSettings", ...args),
getOS: ipcRenderer.sendSync("setup-getOS") as string, // String as far as I care.
saveSettings: (...args: [Settings]) => ipcRenderer.send("saveSettings", ...args),
getLang: (toGet: string) =>
ipcRenderer.invoke("getLang", toGet).then((result) => {
ipcRenderer.invoke("getLang", toGet).then((result: string) => {
return result;
})
});

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -123,9 +123,8 @@
});
if (window.armcordinternal.getOS == "linux") {
document.getElementById("tray").value = "false";
document.getElementById(
"linuxNotice"
).innerHTML = `Linux may not work well with tray icons. Depending on your system configuration, you may not be able to see the tray icon. Enable at your own risk. Can be changed later.`;
document.getElementById("linuxNotice").innerHTML =
`Linux may not work well with tray icons. Depending on your system configuration, you may not be able to see the tray icon. Enable at your own risk. Can be changed later.`;
}
document.getElementById("next-page4").addEventListener("click", () => {
window.armcordinternal.saveSettings({

View file

@ -19,5 +19,5 @@ export async function createSplashWindow(): Promise<void> {
preload: path.join(import.meta.dirname, "preload.mjs")
}
});
splashWindow.loadFile(path.join(import.meta.dirname, "splash.html"));
await splashWindow.loadFile(path.join(import.meta.dirname, "splash.html"));
}

View file

@ -2,10 +2,10 @@ import {contextBridge, ipcRenderer} from "electron";
contextBridge.exposeInMainWorld("internal", {
restart: () => ipcRenderer.send("restart"),
installState: ipcRenderer.sendSync("modInstallState"),
version: ipcRenderer.sendSync("get-app-version", "app-version"),
installState: ipcRenderer.sendSync("modInstallState") as string,
version: ipcRenderer.sendSync("get-app-version", "app-version") as string,
getLang: (toGet: string) =>
ipcRenderer.invoke("getLang", toGet).then((result) => {
ipcRenderer.invoke("getLang", toGet).then((result: string) => {
return result;
}),
splashEnd: () => ipcRenderer.send("splashEnd")

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Loading</title>

View file

@ -53,6 +53,7 @@ body {
font-family: "Whitney", sans-serif;
box-sizing: border-box;
-webkit-user-select: none;
user-select: none;
cursor: default;
}

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View file

@ -1,36 +1,22 @@
import {BrowserWindow, app, dialog, ipcMain, shell} from "electron";
import path from "path";
import fs from "fs";
import {sleep} from "../common/sleep.js";
import {createInviteWindow, mainWindow} from "../discord/window.js";
import type {ThemeManifest} from "../types/themeManifest.d.js";
let themeWindow: BrowserWindow;
let instance = 0;
interface ThemeManifest {
name?: string;
author?: string;
description?: string;
version?: string;
invite?: string;
authorId?: string;
theme: string;
authorLink?: string;
donate?: string;
patreon?: string;
website?: string;
source?: string;
updateSrc?: string;
supportsArmCordTitlebar?: boolean;
}
function parseBDManifest(content: string) {
const metaReg = /@([^ ]*) (.*)/g;
if (!content.startsWith("/**")) {
throw new Error("Not a manifest.");
}
let manifest: ThemeManifest = {theme: "src.css"};
const manifest: ThemeManifest = {theme: "src.css", name: "null"}; // Will be defined later
let match;
while ((match = metaReg.exec(content)) !== null) {
let [_, key, value] = match;
const [_, key] = match;
let [value] = match;
if (key === "import") break;
value = value.trim();
@ -88,7 +74,7 @@ function parseBDManifest(content: string) {
}
const userDataPath = app.getPath("userData");
const themesPath = path.join(userDataPath, "/themes/");
export function createTManagerWindow(): void {
export async function createTManagerWindow(): Promise<void> {
console.log("Creating theme manager window.");
instance += 1;
if (instance > 1) {
@ -118,13 +104,13 @@ export function createTManagerWindow(): void {
if (url.startsWith("https://discord.gg/")) {
createInviteWindow(url.replace("https://discord.gg/", ""));
} else {
shell.openExternal(url);
void shell.openExternal(url);
}
}
});
async function managerLoadPage(): Promise<void> {
themeWindow.loadFile(`${import.meta.dirname}/manager.html`);
await themeWindow.loadFile(`${import.meta.dirname}/manager.html`);
}
const userDataPath = app.getPath("userData");
const themesFolder = `${userDataPath}/themes/`;
@ -135,27 +121,24 @@ export function createTManagerWindow(): void {
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
}
ipcMain.on("openThemesFolder", async () => {
ipcMain.on("openThemesFolder", () => {
shell.showItemInFolder(themesPath);
await sleep(1000);
});
ipcMain.on("reloadMain", async () => {
ipcMain.on("reloadMain", () => {
mainWindow.webContents.reload();
});
ipcMain.on("addToDisabled", async (_event, name: string) => {
ipcMain.on("addToDisabled", (_event, name: string) => {
fs.appendFileSync(path.join(userDataPath, "/disabled.txt"), `${name}\n`);
sleep(1000);
});
ipcMain.on("disabled", async (e) => {
ipcMain.on("disabled", (e) => {
e.returnValue = fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
});
ipcMain.on("removeFromDisabled", async (_event, name: string) => {
let e = await fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
ipcMain.on("removeFromDisabled", (_event, name: string) => {
const e = fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), e.replace(name, ""));
sleep(1000);
});
ipcMain.on("uninstallTheme", async (_event, id: string) => {
let themePath = path.join(themesFolder, id);
ipcMain.on("uninstallTheme", (_event, id: string) => {
const themePath = path.join(themesFolder, id);
if (fs.existsSync(themePath)) {
fs.rmdirSync(themePath, {recursive: true});
console.log(`Removed ${id} folder`);
@ -166,11 +149,12 @@ export function createTManagerWindow(): void {
themeWindow.webContents.reload();
mainWindow.webContents.reload();
});
ipcMain.on("installBDTheme", async (_event, link: string) => {
ipcMain.on("installBDTheme", (_event, link: string) => {
return async () => {
try {
let code = await (await fetch(link)).text();
let manifest = parseBDManifest(code);
let themePath = path.join(themesFolder, `${manifest.name?.replace(" ", "-")}-BD`);
const code = await (await fetch(link)).text();
const manifest = parseBDManifest(code);
const themePath = path.join(themesFolder, `${manifest.name?.replace(" ", "-")}-BD`);
if (!fs.existsSync(themePath)) {
fs.mkdirSync(themePath);
console.log(`Created ${manifest.name} folder`);
@ -193,6 +177,7 @@ export function createTManagerWindow(): void {
"Failed to import theme from link. Please make sure that it's a valid BetterDiscord Theme."
);
}
};
});
themeWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => {
@ -206,7 +191,7 @@ export function createTManagerWindow(): void {
});
});
managerLoadPage();
await managerLoadPage();
themeWindow.on("close", () => {
instance = 0;
});

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<style>
@ -213,7 +213,9 @@
border-style: solid;
border-radius: 10px;
width: 80%;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
box-shadow:
0 4px 8px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;

View file

@ -1,15 +1,17 @@
import {ipcRenderer, contextBridge} from "electron";
import {sleep} from "../common/sleep.js";
import {ThemeManifest} from "../types/themeManifest";
contextBridge.exposeInMainWorld("themes", {
install: (url: string) => ipcRenderer.send("installBDTheme", url),
uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id)
});
ipcRenderer.on("themeManifest", (_event, json) => {
let manifest = JSON.parse(json);
ipcRenderer.on("themeManifest", (_event, json: string) => {
async () => {
const manifest = JSON.parse(json) as ThemeManifest;
console.log(manifest);
sleep(1000);
let e = document.getElementById("cardBox");
let id = manifest.name.replace(" ", "-");
await sleep(1000); // REVIEW - This is all that requires async, would be nice if it could be removed.
const e = document.getElementById("cardBox");
const id = manifest.name.replace(" ", "-");
e?.insertAdjacentHTML(
"beforeend",
`
@ -28,9 +30,8 @@ ipcRenderer.on("themeManifest", (_event, json) => {
document.getElementById("themeInfoName")!.textContent = `${manifest.name} by ${manifest.author}`;
document.getElementById("themeInfoDesc")!.textContent = `${manifest.description}\n\n${manifest.version}`;
if (manifest.supportsArmCordTitlebar !== undefined) {
document.getElementById(
"themeInfoButtons"
)!.innerHTML += `<img class="themeInfoIcon" id="removeTheme" onclick="themes.uninstall('${id}')" title="Remove the theme" src="https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/Trash.png"></img>
document.getElementById("themeInfoButtons")!.innerHTML +=
`<img class="themeInfoIcon" id="removeTheme" onclick="themes.uninstall('${id}')" title="Remove the theme" src="https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/Trash.png"></img>
<img class="themeInfoIcon" id="updateTheme" onclick="themes.install('${manifest.updateSrc}')" title="Update your theme" src="https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/UpgradeArrow.png"></img>
<img class="themeInfoIcon" id="compatibility" title="Supports ArmCord Titlebar" src=""></img>`;
console.log("e");
@ -43,22 +44,19 @@ ipcRenderer.on("themeManifest", (_event, json) => {
}
}
if (manifest.source != undefined)
document.getElementById(
"themeInfoButtons"
)!.innerHTML += `<a href="${manifest.source}" class="button">Source code</a>`;
document.getElementById("themeInfoButtons")!.innerHTML +=
`<a href="${manifest.source}" class="button">Source code</a>`;
if (manifest.website != undefined)
document.getElementById(
"themeInfoButtons"
)!.innerHTML += `<a href="${manifest.website}" class="button">Website</a>`;
document.getElementById("themeInfoButtons")!.innerHTML +=
`<a href="${manifest.website}" class="button">Website</a>`;
if (manifest.invite != undefined)
document.getElementById(
"themeInfoButtons"
)!.innerHTML += `<a href="${`https://discord.gg/${manifest.invite}`}" class="button">Support Discord</a>`;
document.getElementById("themeInfoButtons")!.innerHTML +=
`<a href="${`https://discord.gg/${manifest.invite}`}" class="button">Support Discord</a>`;
});
if (!ipcRenderer.sendSync("disabled").includes(id)) {
if (!(ipcRenderer.sendSync("disabled") as string[]).includes(id)) {
(document.getElementById(id) as HTMLInputElement).checked = true;
}
(document.getElementById(id) as HTMLInputElement)!.addEventListener("input", function (evt) {
(document.getElementById(id) as HTMLInputElement).addEventListener("input", function () {
ipcRenderer.send("reloadMain");
if (this.checked) {
ipcRenderer.send("removeFromDisabled", id);
@ -66,6 +64,7 @@ ipcRenderer.on("themeManifest", (_event, json) => {
ipcRenderer.send("addToDisabled", id);
}
});
};
});
document.addEventListener("DOMContentLoaded", () => {
document.getElementsByClassName("close")[0].addEventListener("click", () => {
@ -73,6 +72,6 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("themeInfoButtons")!.innerHTML = "";
});
document.getElementById("download")!.addEventListener("click", () => {
ipcRenderer.send("installBDTheme", (document.getElementById("themeLink") as HTMLInputElement)!.value);
ipcRenderer.send("installBDTheme", (document.getElementById("themeLink") as HTMLInputElement).value);
});
});

View file

@ -1,20 +1,20 @@
import * as fs from "fs";
import fs from "fs";
import {Menu, MessageBoxOptions, Tray, app, dialog, nativeImage} from "electron";
import {createInviteWindow, mainWindow} from "./discord/window.js";
import * as path from "path";
import path from "path";
import {createSettingsWindow} from "./settings/main.js";
import {getConfig, getConfigLocation, setConfig} from "./common/config.js";
import {getDisplayVersion} from "./common/version.js";
export let tray: any = null;
export let tray: Tray;
let trayIcon = "ac_plug_colored";
app.whenReady().then(async () => {
let finishedSetup = await getConfig("doneSetup");
if ((await getConfig("trayIcon")) != "default") {
trayIcon = await getConfig("trayIcon");
void app.whenReady().then(async () => {
// REVIEW - app will hang at startup if line above is awaited.
const finishedSetup = getConfig("doneSetup");
if (getConfig("trayIcon") != "default") {
trayIcon = getConfig("trayIcon");
}
let trayPath = nativeImage.createFromPath(path.join(import.meta.dirname, "../", `/assets/${trayIcon}.png`));
let trayVerIcon;
trayVerIcon = function () {
const trayVerIcon = function () {
if (process.platform == "win32") {
return trayPath.resize({height: 16});
} else if (process.platform == "darwin") {
@ -26,8 +26,8 @@ app.whenReady().then(async () => {
};
if (process.platform == "darwin" && trayPath.getSize().height > 22) trayPath = trayPath.resize({height: 22});
if (await getConfig("tray")) {
let clientName = (await getConfig("clientName")) ?? "ArmCord";
if (getConfig("tray")) {
const clientName = getConfig("clientName") ?? "ArmCord";
tray = new Tray(trayPath);
if (finishedSetup == false) {
const contextMenu = Menu.buildFromTemplate([
@ -37,8 +37,8 @@ app.whenReady().then(async () => {
},
{
label: `Quit ${clientName}`,
async click() {
fs.unlink(await getConfigLocation(), (err) => {
click() {
fs.unlink(getConfigLocation(), (err) => {
if (err) throw err;
console.log('Closed during setup. "settings.json" was deleted');
@ -50,6 +50,7 @@ app.whenReady().then(async () => {
tray.setContextMenu(contextMenu);
} else {
const contextMenu = Menu.buildFromTemplate([
// REVIEW - Awaiting any window creation will fail silently
{
label: `${clientName} ${getDisplayVersion()}`,
icon: trayVerIcon(),
@ -67,13 +68,13 @@ app.whenReady().then(async () => {
{
label: "Open Settings",
click() {
createSettingsWindow();
void createSettingsWindow();
}
},
{
label: "Support Discord Server",
click() {
createInviteWindow("TnhxcqynZ2");
void createInviteWindow("TnhxcqynZ2");
}
},
{
@ -82,7 +83,8 @@ app.whenReady().then(async () => {
{
label: `Quit ${clientName}`,
click() {
app.quit();
// NOTE - Temporary fix for app not actually quitting
app.exit();
}
}
]);
@ -93,7 +95,7 @@ app.whenReady().then(async () => {
mainWindow.show();
});
} else {
if ((await getConfig("tray")) == undefined) {
if (getConfig("tray") == undefined) {
if (process.platform == "linux") {
const options: MessageBoxOptions = {
type: "question",
@ -104,7 +106,7 @@ app.whenReady().then(async () => {
detail: "Linux may not work well with tray icons. Depending on your system configuration, you may not be able to see the tray icon. Enable at your own risk. Can be changed later."
};
dialog.showMessageBox(mainWindow, options).then(({response}) => {
await dialog.showMessageBox(mainWindow, options).then(({response}) => {
if (response == 0) {
setConfig("tray", true);
} else {

21
src/types/armcordWindow.d.ts vendored Normal file
View file

@ -0,0 +1,21 @@
export interface ArmCordWindow {
window: {
show: () => void;
hide: () => void;
minimize: () => void;
maximize: () => void;
};
titlebar: {
injectTitlebar: () => void;
isTitlebar: boolean;
};
electron: string;
channel: string;
setPingCount: (pingCount: number) => void;
setTrayIcon: (favicon: string) => void;
getLang: (toGet: string) => Promise<string>;
getDisplayMediaSelector: () => Promise<string>;
version: string;
mods: string;
openSettingsWindow: () => void;
}

1
src/types/i18nStrings.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export type i18nStrings = Record<string, string>;

31
src/types/settings.d.ts vendored Normal file
View file

@ -0,0 +1,31 @@
export interface Settings {
// Referenced for detecting a broken config.
"0"?: string;
// Referenced once for disabling mod updating.
noBundleUpdates?: boolean;
// Only used for external url warning dialog.
ignoreProtocolWarning?: boolean;
customIcon: string;
windowStyle: string;
channel: string;
armcordCSP: boolean;
minimizeToTray: boolean;
multiInstance: boolean;
spellcheck: boolean;
mods: string;
dynamicIcon: boolean;
mobileMode: boolean;
skipSplash: boolean;
performanceMode: string;
customJsBundle: RequestInfo | URL;
customCssBundle: RequestInfo | URL;
startMinimized: boolean;
useLegacyCapturer: boolean;
tray: boolean;
keybinds: string[];
inviteWebsocket: boolean;
disableAutogain: boolean;
trayIcon: string;
doneSetup: boolean;
clientName: string;
}

16
src/types/themeManifest.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
export interface ThemeManifest {
name: string;
author?: string;
description?: string;
version?: string;
invite?: string;
authorId?: string;
theme: string;
authorLink?: string;
donate?: string;
patreon?: string;
website?: string;
source?: string;
updateSrc?: string;
supportsArmCordTitlebar?: boolean;
}

7
src/types/windowState.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
export interface WindowState {
width: number;
height: number;
x: number;
y: number;
isMaximized: boolean;
}

View file

@ -1,6 +1,6 @@
{
// Reference: https://www.typescriptlang.org/tsconfig
"include": ["src/**/*"], // This makes it so that the compiler won't compile anything outside of "src".
"include": ["src/**/*", "eslint.config.js", "prettier.config.js"], // This makes it so that the compiler won't compile anything outside of "src".
//"exclude": ["src/**/*.test.ts"], // Exclude .test.ts files since they're for Jest only.
"compilerOptions": {
// Project Structure //
@ -14,10 +14,10 @@
"noFallthroughCasesInSwitch": true, // Prevents accidentally forgetting to break every switch case. Of course, if you know what you're doing, feel free to add a @ts-ignore, which also signals that it's not a mistake.
"forceConsistentCasingInFileNames": true, // Make import paths case-sensitive. "./tEst" is no longer the same as "./test".
"esModuleInterop": true, // Enables compatibility with Node.js' module system since the entire export can be whatever you want. allowSyntheticDefaultImports doesn't address runtime issues and is made redundant by this setting.
"lib": ["ES2022", "DOM"], // Specifies what common libraries you have access to. If you're working in Node.js, you'll want to leave out the DOM library. But do make sure to include "@types/node" because otherwise, variables like "console" won't be defined.
"lib": ["ESNext", "DOM"], // Specifies what common libraries you have access to. If you're working in Node.js, you'll want to leave out the DOM library. But do make sure to include "@types/node" because otherwise, variables like "console" won't be defined.
"noUnusedLocals": true, // Warns you if you have unused variables.
// Output //
"module": "ES2022", // Compiles ES6 imports to require() syntax.
"module": "ESNext", // Compiles ES6 imports to require() syntax.
"removeComments": false,
"sourceMap": true, // Used for displaying the original source when debugging in webpack. Allows you to set breakpoints directly on TypeScript code for VSCode's debugger.
@ -26,7 +26,7 @@
"declarationMap": false, // Allows the user to go to the source file when hitting a go-to-implementation key like F12 in VSCode for example.
//"declarationDir": "typings", // declarationDir allows you to separate the compiled code from the declaration files, used in conjunction with package.json's "types" property.
// Web Compatibility //
"target": "ES2022", // ES2017 supports async/await, reducing the amount of compiled code, especially for async-heavy projects. ES2020 is from the Node 14 base (https://github.com/tsconfig/bases/blob/master/bases/node14.json)
"target": "ESNext", // ES2017 supports async/await, reducing the amount of compiled code, especially for async-heavy projects. ES2020 is from the Node 14 base (https://github.com/tsconfig/bases/blob/master/bases/node14.json)
"downlevelIteration": false, // This flag adds extra support when targeting ES3, but adds extra bloat otherwise.
"importHelpers": false // Reduce the amount of bloat that comes from downlevelIteration (when polyfills are redeclared).
}