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" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
- package-ecosystem: npm - package-ecosystem: npm
directory: "/" directory: "/"
schedule: schedule:

3
.github/release.md vendored
View file

@ -1,3 +1,4 @@
# Thanks for checking out ArmCord dev builds! # 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.
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! Make sure to join our [Discord server](https://discord.gg/uaW5vMY3V6) to share opinions, or to chat with ArmCord developers!

View file

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

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

@ -1,11 +1,11 @@
name: Release build name: Release build
on: on:
push: push:
branches: branches:
- stable - stable
env: env:
FORCE_COLOR: true FORCE_COLOR: true
jobs: jobs:
build-linux: build-linux:
@ -13,69 +13,67 @@ jobs:
steps: steps:
- name: Checkout code - 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 - name: Use Node.js 22
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 22
cache: "pnpm" cache: "pnpm"
- name: Install Node dependencies - name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install run: pnpm i -g cargo-cp-artifact electron-builder && pnpm i
- name: Install Electron-Builder
run: pnpm install -g electron-builder
- name: Build - 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: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: List all files in the dist directory - name: List all files in the dist directory
run: ls -l dist run: ls -l dist
- name: Delete unpacked builds - name: Delete unpacked builds
run: rm -rf dist/linux-unpacked && rm -rf dist/linux-arm64-unpacked && rm -rf dist/linux-armv7l-unpacked run: rm -rf dist/linux-unpacked && rm -rf dist/linux-arm64-unpacked && rm -rf dist/linux-armv7l-unpacked
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: ArmCordLinux name: ArmCordLinux
path: dist/ path: dist/
build-mac: build-mac:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout code - 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 - name: Use Node.js 22
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 22
cache: "pnpm" cache: "pnpm"
- name: Install Node dependencies - name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install run: pnpm i -g cargo-cp-artifact electron-builder && pnpm i
- name: Install Electron-Builder
run: pnpm install -g electron-builder
- name: Build - name: Build
run: npm run build && electron-builder --macos --x64 --arm64 run: pnpm run build && electron-builder --macos --x64 --arm64
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: List all files in the dist directory - name: List all files in the dist directory
run: ls -l dist run: ls -l dist
- name: Delete unpacked builds - name: Delete unpacked builds
run: rm -rf dist/macos-unpacked run: rm -rf dist/macos-unpacked
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: ArmCordMac name: ArmCordMac
path: dist/ path: dist/
@ -84,62 +82,62 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout code - 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 - name: Use Node.js 22
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 22
cache: "pnpm" cache: "pnpm"
- name: Install Node dependencies - name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install run: pnpm i -g cargo-cp-artifact electron-builder && pnpm i
- name: Install Electron-Builder
run: pnpm install -g electron-builder
- name: Build - name: Build
run: npm run build && electron-builder --windows run: pnpm run build && electron-builder --windows
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete unpacked builds - name: Delete unpacked builds
run: Remove-Item -LiteralPath ".\dist\win-unpacked" -Force -Recurse run: Remove-Item -LiteralPath ".\dist\win-unpacked" -Force -Recurse
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: ArmCordWindows name: ArmCordWindows
path: dist/ path: dist/
build-windowsOnARM: build-windowsOnARM:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Checkout code - 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 - name: Set architecture
run: set npm_config_arch=arm64 run: set npm_config_arch=arm64
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Install Node dependencies - name: Install Node dependencies
run: pnpm install -g cargo-cp-artifact && pnpm install run: pnpm install -g cargo-cp-artifact electron-builder && pnpm install
- name: Install Electron-Builder
run: pnpm install -g electron-builder
- name: Build - name: Build
run: npm run build && electron-builder --windows --arm64 run: pnpm run build && electron-builder --windows --arm64
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete unpacked builds - name: Delete unpacked builds
run: Remove-Item -LiteralPath ".\dist\win-arm64-unpacked" -Force -Recurse run: Remove-Item -LiteralPath ".\dist\win-arm64-unpacked" -Force -Recurse
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: ArmCordWindowsArm64 name: ArmCordWindowsArm64
path: dist/ path: dist/
@ -148,86 +146,93 @@ jobs:
needs: [build-linux, build-mac, build-windows, build-windowsOnARM] needs: [build-linux, build-mac, build-windows, build-windowsOnARM]
steps: steps:
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v4
with: with:
name: ArmCordMac name: ArmCordMac
path: macos path: macos
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with: with:
name: ArmCordWindows name: ArmCordWindows
path: windows path: windows
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with: with:
name: ArmCordWindowsArm64 name: ArmCordWindowsArm64
path: windows path: windows
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with: with:
name: ArmCordLinux name: ArmCordLinux
path: linux path: linux
- name: ls - name: ls
run: ls run: ls
- name: Delete unwanted directories - name: Delete unwanted directories
run: rm -rf {linux,macos,windows}/*/ run: rm -rf {linux,macos,windows}/*/
rm -rf {linux,macos,windows}/.icon* rm -rf {linux,macos,windows}/.icon*
rm -rf {linux,macos,windows}/builder-debug.yml rm -rf {linux,macos,windows}/builder-debug.yml
- name: ls dirs - name: ls dirs
run: ls linux && ls macos && ls windows run: ls linux && ls macos && ls windows
- name: Get some values needed for the release - name: Get some values needed for the release
id: vars id: vars
shell: bash shell: bash
run: | run: |
echo "::set-output name=releaseTag::$(git describe --tags --abbrev=0)" echo "::set-output name=releaseTag::$(git describe --tags --abbrev=0)"
- name: Create Release - name: Create Release
uses: actions/github-script@v2 uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
console.log('environment', process.versions); console.log('environment', process.versions);
const fs = require('fs').promises;
const { repo: { owner, repo }, sha } = context;
console.log({ owner, repo, sha });
const release = await github.repos.createRelease({ const fs = require('fs').promises;
owner, repo,
tag_name: process.env.releaseTag,
draft: true,
target_commitish: sha
});
console.log('created release', { release }); const { repo: { owner, repo }, sha } = context;
console.log({ owner, repo, sha });
for (let file of await fs.readdir('linux')) {
// do whatever filtering you want here, I'm just uploading all the files const release = await github.repos.createRelease({
console.log('uploading', file); owner, repo,
await github.repos.uploadReleaseAsset({ tag_name: process.env.releaseTag,
owner, repo, draft: true,
release_id: release.data.id, target_commitish: sha
name: file, });
data: await fs.readFile(`./linux/${file}`)
}); console.log('created release', { release });
}
for (let file of await fs.readdir('windows')) { for (let file of await fs.readdir('linux')) {
// do whatever filtering you want here, I'm just uploading all the files // do whatever filtering you want here, I'm just uploading all the files
console.log('uploading', file); console.log('uploading', file);
await github.repos.uploadReleaseAsset({ await github.repos.uploadReleaseAsset({
owner, repo, owner, repo,
release_id: release.data.id, release_id: release.data.id,
name: file, name: file,
data: await fs.readFile(`./windows/${file}`) data: await fs.readFile(`./linux/${file}`)
}); });
} }
for (let file of await fs.readdir('macos')) { for (let file of await fs.readdir('windows')) {
// do whatever filtering you want here, I'm just uploading all the files // do whatever filtering you want here, I'm just uploading all the files
console.log('uploading', file); console.log('uploading', file);
await github.repos.uploadReleaseAsset({ await github.repos.uploadReleaseAsset({
owner, repo, owner, repo,
release_id: release.data.id, release_id: release.data.id,
name: file, name: file,
data: await fs.readFile(`./macos/${file}`) data: await fs.readFile(`./windows/${file}`)
}); });
} }
for (let file of await fs.readdir('macos')) {
// do whatever filtering you want here, I'm just uploading all the files
console.log('uploading', file);
await github.repos.uploadReleaseAsset({
owner, repo,
release_id: release.data.id,
name: file,
data: await fs.readFile(`./macos/${file}`)
});
}
env: env:
releaseTag: ${{ steps.vars.outputs.releaseTag }} releaseTag: ${{ steps.vars.outputs.releaseTag }}

View file

@ -1,13 +1,13 @@
name: Publish to WinGet name: Publish to WinGet
on: on:
release: release:
types: [released] types: [released]
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v2 - uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: ArmCord.ArmCord identifier: ArmCord.ArmCord
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}

1
.gitignore vendored
View file

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

View file

@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
set -e set -e
npm run format pnpm run format
git add -A 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

@ -1,17 +1,17 @@
<div align="center"> <div align="center">
<img src="https://armcord.app/logo.png" width="520"> <img src="https://armcord.app/logo.png" width="520">
<br>ArmCord is a custom client designed to enhance your Discord experience while keeping everything lightweight. <br>ArmCord is a custom client designed to enhance your Discord experience while keeping everything lightweight.
</div> </div>
# Features # Features
- **Standalone client** - **Standalone client**
ArmCord is built as a standalone client and doesn't rely on the original Discord client in any way. ArmCord is built as a standalone client and doesn't rely on the original Discord client in any way.
- **Various mods built-in** - **Various mods built-in**
Enjoy [Vencord](https://github.com/Vendicated/Vencord), [Shelter](https://github.com/uwu/shelter) and their many features, or have a more vanilla experience, it's your choice! Enjoy [Vencord](https://github.com/Vendicated/Vencord), [Shelter](https://github.com/uwu/shelter) and their many features, or have a more vanilla experience, it's your choice!
- **Themes** - **Themes**
@ -25,7 +25,7 @@
- **Supports Rich Presence** - **Supports Rich Presence**
Unlike other clients, ArmCord supports rich presence (game activity) out of the box thanks to [arRPC](https://arrpc.openasar.dev). Unlike other clients, ArmCord supports rich presence (game activity) out of the box thanks to [arRPC](https://arrpc.openasar.dev).
- **Mobile support** - **Mobile support**
ArmCord has **experimental** mobile support for phones running Linux such as the PinePhone. While this is still far from an ideal solution, we're slowly trying to improve it. ArmCord has **experimental** mobile support for phones running Linux such as the PinePhone. While this is still far from an ideal solution, we're slowly trying to improve it.
@ -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. 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!** - **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)! 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? # How to run/install it?
## Packaging status ## Packaging status
[![Packaging status](https://repology.org/badge/vertical-allrepos/armcord.svg)](https://repology.org/project/armcord/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/armcord.svg)](https://repology.org/project/armcord/versions)
### Windows ### Windows
<a href="https://microsoft.com/store/apps/9PFHLJFD7KJT"> <a href="https://microsoft.com/store/apps/9PFHLJFD7KJT">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" alt="Download ArmCord" /> <img src="https://get.microsoft.com/images/en-us%20dark.svg" alt="Download ArmCord" />
</a> </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). If you're using an older version of Windows, you need to use [pre-built installers](https://www.armcord.app/download).
### Flatpak ### 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> <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 ### 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: 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 ```sh
curl -fsSL https://apt.armcord.app/public.gpg | sudo gpg --dearmor -o /usr/share/keyrings/armcord.gpg 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 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 update
sudo apt install armcord sudo apt install armcord
``` ```
If you previously used old ArmCord apt repo, here's how you can remove it: If you previously used old ArmCord apt repo, here's how you can remove it:
```sh ```sh
sudo rm /etc/apt/sources.list.d/armcord.list sudo rm /etc/apt/sources.list.d/armcord.list
sudo rm /usr/share/keyrings/armcord.gpg sudo rm /usr/share/keyrings/armcord.gpg
sudo apt update sudo apt update
``` ```
### Snap package ### Snap package
ArmCord is also available on the Snap store [here](https://snapcraft.io/armcord). ArmCord is also available on the Snap store [here](https://snapcraft.io/armcord).
<a href="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" /> <img alt="Get it from the Snap Store" src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg" />
</a> </a>
Similar to `armcord-git` on AUR, you can install the latest dev builds through snaps by running this command: Similar to `armcord-git` on AUR, you can install the latest dev builds through snaps by running this command:
```shell ```shell
sudo snap install armcord --channel=latest/edge sudo snap install armcord --channel=latest/edge
``` ```
Snapd will automatically update the app including developer builds. Snapd will automatically update the app including developer builds.
### Winget Package ### Winget Package
ArmCord is also available on the [winget-pkgs](https://github.com/microsoft/winget-pkgs) repository: ArmCord is also available on the [winget-pkgs](https://github.com/microsoft/winget-pkgs) repository:
```
```ps1
winget install ArmCord.ArmCord winget install ArmCord.ArmCord
``` ```
### Scoop package ### Scoop package
ArmCord is also available on [Scoop extras](https://github.com/ScoopInstaller/Extras) repo: ArmCord is also available on [Scoop extras](https://github.com/ScoopInstaller/Extras) repo:
```
```ps1
scoop bucket add extras scoop bucket add extras
``` ```
```
```ps1
scoop install armcord scoop install armcord
``` ```
### AUR Package ### AUR Package
ArmCord is also available on the Arch User Repository (AUR): 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-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 - [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`. Install it via an AUR helper tool like `yay`.
**Example:** `yay -S armcord-bin` **Example:** `yay -S armcord-bin`
### Homebrew repository ### Homebrew repository
ArmcCord also has a homebrew repository
``` ArmCord also has a homebrew repository
```zsh
brew tap armcord/armcord brew tap armcord/armcord
``` ```
```
```zsh
brew install --cask armcord brew install --cask armcord
``` ```
### FreeBSD ### FreeBSD
You can also get ArmCord running on FreeBSD by following [these instructions](https://gist.github.com/axyiee/4d29c982ac85d5d26f98a51040b5de37). You can also get ArmCord running on FreeBSD by following [these instructions](https://gist.github.com/axyiee/4d29c982ac85d5d26f98a51040b5de37).
### Pi-Apps ### Pi-Apps
ArmCord is also available in [Pi-Apps](https://github.com/Botspot/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) [![badge](https://github.com/Botspot/pi-apps/blob/master/icons/badge.png?raw=true)](https://github.com/Botspot/pi-apps)
### Pre-built binaries: ### Pre-built binaries:
Check the **releases tab** for precompiled packages for Linux, Windows, and Mac OS. Alternatively, use our Sourceforge mirror. 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> <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: ### 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`
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 `pnpm run build`
4. Compile/Package with `pnpm run package`
# FAQ # FAQ
## Do you have a support Discord? ## Do you have a support Discord?
[![](https://dcbadge.vercel.app/api/server/TnhxcqynZ2)](https://discord.gg/TnhxcqynZ2) [![](https://dcbadge.vercel.app/api/server/TnhxcqynZ2)](https://discord.gg/TnhxcqynZ2)
## Will I get banned for using this?
## 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. - 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? ## 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. - Yes! ArmCord should work normally under Windows, MacOS, and Linux as long as it has Electron support.
## How can I access the settings? ## 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` - 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? ## 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 - 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). - 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? ## Where can I find the source code?
- The source code is on [GitHub](https://github.com/ArmCord/ArmCord/). - The source code is on [GitHub](https://github.com/ArmCord/ArmCord/).
## Where can I translate this? ## Where can I translate this?
- Translations are done using our [Weblate page](https://hosted.weblate.org/projects/armcord/armcord/). - Translations are done using our [Weblate page](https://hosted.weblate.org/projects/armcord/armcord/).
# Credits # Credits
- [ArmCord UI design, branding, and a few features](https://github.com/kckarnige) - [ArmCord UI design, branding, and a few features](https://github.com/kckarnige)
- [OpenAsar](https://github.com/GooseMod/OpenAsar) - [OpenAsar](https://github.com/GooseMod/OpenAsar)
- [arRPC (for Rich Presence)](https://github.com/OpenAsar/arrpc) - [arRPC (for Rich Presence)](https://github.com/OpenAsar/arrpc)
@ -162,4 +203,4 @@ ArmCord is also available in [Pi-Apps](https://github.com/Botspot/pi-apps).
- (Pre v3.0.0) [custom-electron-titlebar](https://github.com/AlexTorresSk/custom-electron-titlebar) - (Pre v3.0.0) [custom-electron-titlebar](https://github.com/AlexTorresSk/custom-electron-titlebar)
- [electron-builder](https://electron.build) - [electron-builder](https://electron.build)
Discord is trademark of Discord Inc. ArmCord is not affiliated with or endorsed by Discord Inc. Discord is trademark of Discord Inc. ArmCord is not affiliated with or endorsed by Discord Inc.

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

View file

@ -1,6 +1,7 @@
import {app, dialog} from "electron"; import {app, dialog} from "electron";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import type {Settings} from "../types/settings.d.js";
import {getWindowStateLocation} from "./windowState.js"; import {getWindowStateLocation} from "./windowState.js";
export let firstRun: boolean; export let firstRun: boolean;
export function checkForDataFolder(): void { 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 { export function getConfigLocation(): string {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}settings.json`; return `${storagePath}settings.json`;
} }
export async function getConfig<K extends keyof Settings>(object: K): Promise<Settings[K]> { // REVIEW - If I remember correctly fs doesn't need async. I have adjusted the Promise<Settings[K]> to reflect so.
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8"); // Why touch it when it worked fine? The Async-ness of this function caused headaches in a lot of other places.
let returndata = JSON.parse(rawdata); // Tested with src/tray.ts - Seems to work great!
return returndata[object]; // 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) { export function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): void {
let rawdata = fs.readFileSync(getConfigLocation(), "utf-8"); const rawData = fs.readFileSync(getConfigLocation(), "utf-8");
let returndata = JSON.parse(rawdata); const parsed = JSON.parse(rawData) as Settings;
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);
parsed[object] = toSet; parsed[object] = toSet;
let toSave = JSON.stringify(parsed, null, 4); const toSave = JSON.stringify(parsed, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8"); fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
} }
export async function setConfigBulk(object: Settings): Promise<void> { export function setConfigBulk(object: Settings): void {
let existingData = {}; let existingData = {};
try { try {
const existingDataBuffer = fs.readFileSync(getConfigLocation(), "utf-8"); const existingDataBuffer = fs.readFileSync(getConfigLocation(), "utf-8");
existingData = JSON.parse(existingDataBuffer.toString()); existingData = JSON.parse(existingDataBuffer.toString()) as Settings;
} catch (error) { } catch (error) {
// Ignore errors when the file doesn't exist or parsing fails // 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); const toSave = JSON.stringify(mergedData, null, 4);
fs.writeFileSync(getConfigLocation(), toSave, "utf-8"); fs.writeFileSync(getConfigLocation(), toSave, "utf-8");
} }
export async function checkIfConfigExists(): Promise<void> { export function checkIfConfigExists(): void {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}settings.json`; const settingsFile = `${storagePath}settings.json`;
@ -91,7 +60,7 @@ export async function checkIfConfigExists(): Promise<void> {
console.log("First run of the ArmCord. Starting setup."); console.log("First run of the ArmCord. Starting setup.");
setup(); setup();
firstRun = true; firstRun = true;
} else if ((await getConfig("doneSetup")) == false) { } else if (getConfig("doneSetup") == false) {
console.log("First run of the ArmCord. Starting setup."); console.log("First run of the ArmCord. Starting setup.");
setup(); setup();
firstRun = true; firstRun = true;
@ -99,9 +68,9 @@ export async function checkIfConfigExists(): Promise<void> {
console.log("ArmCord has been run before. Skipping setup."); console.log("ArmCord has been run before. Skipping setup.");
} }
} }
export async function checkIfConfigIsBroken(): Promise<void> { export function checkIfConfigIsBroken(): void {
try { try {
let settingsData = fs.readFileSync(getConfigLocation(), "utf-8"); const settingsData = fs.readFileSync(getConfigLocation(), "utf-8");
JSON.parse(settingsData); JSON.parse(settingsData);
console.log("Config is fine"); console.log("Config is fine");
} catch (e) { } catch (e) {
@ -114,7 +83,7 @@ export async function checkIfConfigIsBroken(): Promise<void> {
); );
} }
try { try {
let windowData = fs.readFileSync(getWindowStateLocation(), "utf-8"); const windowData = fs.readFileSync(getWindowStateLocation(), "utf-8");
JSON.parse(windowData); JSON.parse(windowData);
console.log("Window config is fine"); console.log("Window config is fine");
} catch (e) { } catch (e) {

View file

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

View file

@ -2,7 +2,7 @@ import {app} from "electron";
import {getConfig} from "./config.js"; import {getConfig} from "./config.js";
export let transparency: boolean; export let transparency: boolean;
export async function injectElectronFlags(): Promise<void> { export function injectElectronFlags(): void {
// MIT License // MIT License
// Copyright (c) 2022 GooseNest // 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? 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" 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": case "performance":
console.log("Performance mode enabled"); console.log("Performance mode enabled");
app.commandLine.appendArgument(presets.performance); app.commandLine.appendArgument(presets.performance);
@ -41,7 +41,7 @@ export async function injectElectronFlags(): Promise<void> {
default: default:
console.log("No performance modes set"); console.log("No performance modes set");
} }
if ((await getConfig("windowStyle")) == "transparent" && process.platform === "win32") { if (getConfig("windowStyle") == "transparent" && process.platform === "win32") {
transparency = true; transparency = true;
} }
} }

View file

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

View file

@ -1,34 +1,31 @@
import {app} from "electron"; import {app} from "electron";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
export interface WindowState { import {WindowState} from "../types/windowState";
width: number;
height: number;
x: number;
y: number;
isMaximized: boolean;
}
export function getWindowStateLocation() { export function getWindowStateLocation() {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
return `${storagePath}window.json`; return `${storagePath}window.json`;
} }
export async function setWindowState(object: WindowState): Promise<void> { export function setWindowState(object: WindowState): void {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
const saveFile = `${storagePath}window.json`; 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"); 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 userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
const settingsFile = `${storagePath}window.json`; const settingsFile = `${storagePath}window.json`;
if (!fs.existsSync(settingsFile)) { if (!fs.existsSync(settingsFile)) {
fs.writeFileSync(settingsFile, "{}", "utf-8"); fs.writeFileSync(settingsFile, "{}", "utf-8");
} }
let rawdata = fs.readFileSync(settingsFile, "utf-8"); const rawData = fs.readFileSync(settingsFile, "utf-8");
let returndata = JSON.parse(rawdata); const returnData = JSON.parse(rawData) as WindowState;
console.log(`[Window state manager] ${returndata}`); console.log(`[Window state manager] ${JSON.stringify(returnData)}`);
return returndata[object]; return returnData[object];
} }

View file

@ -34,7 +34,9 @@
border-radius: 10px; border-radius: 10px;
width: 80%; width: 80%;
height: 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-name: animatetop;
-webkit-animation-duration: 0.4s; -webkit-animation-duration: 0.4s;
animation-name: animatetop; animation-name: animatetop;

View file

@ -47,7 +47,9 @@
.desktop-capturer-selection__btn:hover, .desktop-capturer-selection__btn:hover,
.desktop-capturer-selection__btn:focus { .desktop-capturer-selection__btn:focus {
background: #7289da; 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; color: #fff;
} }
.desktop-capturer-selection__thumbnail { .desktop-capturer-selection__thumbnail {

View file

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

View file

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

View file

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

View file

@ -1,19 +1,19 @@
//ipc stuff //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 {mainWindow} from "./window.js";
import os from "os"; import os from "os";
import fs from "fs"; import fs from "fs";
import path from "path"; 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 {setLang, getLang, getLangName} from "../common/lang.js";
import {sleep} from "../common/sleep.js";
import {getVersion, getDisplayVersion} from "../common/version.js"; import {getVersion, getDisplayVersion} from "../common/version.js";
import {customTitlebar} from "../main.js"; import {customTitlebar} from "../main.js";
import {createSettingsWindow} from "../settings/main.js"; import {createSettingsWindow} from "../settings/main.js";
import {splashWindow} from "../splash/main.js"; import {splashWindow} from "../splash/main.js";
import {createTManagerWindow} from "../themeManager/main.js"; import {createTManagerWindow} from "../themeManager/main.js";
import {modInstallState} from "./extensions/mods.js"; import {modInstallState} from "./extensions/mods.js";
import {Settings} from "../types/settings.d.js";
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
@ -30,7 +30,7 @@ export function registerIpc(): void {
return getLang(toGet); return getLang(toGet);
}); });
ipcMain.on("open-external-link", (_event, href: string) => { ipcMain.on("open-external-link", (_event, href: string) => {
shell.openExternal(href); void shell.openExternal(href);
}); });
ipcMain.on("setPing", (_event, pingCount: number) => { ipcMain.on("setPing", (_event, pingCount: number) => {
switch (os.platform()) { switch (os.platform()) {
@ -39,7 +39,7 @@ export function registerIpc(): void {
break; break;
case "win32": case "win32":
if (pingCount > 0) { 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"); mainWindow.setOverlayIcon(image, "badgeCount");
} else { } else {
mainWindow.setOverlayIcon(null, "badgeCount"); mainWindow.setOverlayIcon(null, "badgeCount");
@ -80,9 +80,9 @@ export function registerIpc(): void {
ipcMain.on("modInstallState", (event) => { ipcMain.on("modInstallState", (event) => {
event.returnValue = modInstallState; event.returnValue = modInstallState;
}); });
ipcMain.on("splashEnd", async () => { ipcMain.on("splashEnd", () => {
splashWindow.close(); splashWindow.close();
if (await getConfig("startMinimized")) { if (getConfig("startMinimized")) {
mainWindow.hide(); mainWindow.hide();
} else { } else {
mainWindow.show(); mainWindow.show();
@ -92,75 +92,77 @@ export function registerIpc(): void {
app.relaunch(); app.relaunch();
app.exit(); app.exit();
}); });
ipcMain.on("minimizeToTray", async (event) => { ipcMain.on("saveSettings", (_event, args: Settings) => {
event.returnValue = await getConfig("minimizeToTray"); setConfigBulk(args);
}); });
ipcMain.on("channel", async (event) => { ipcMain.on("minimizeToTray", (event) => {
event.returnValue = await getConfig("channel"); event.returnValue = getConfig("minimizeToTray");
}); });
ipcMain.on("clientmod", async (event) => { ipcMain.on("channel", (event) => {
event.returnValue = await getConfig("mods"); event.returnValue = getConfig("channel");
}); });
ipcMain.on("legacyCapturer", async (event) => { ipcMain.on("clientmod", (event) => {
event.returnValue = await getConfig("useLegacyCapturer"); event.returnValue = getConfig("mods");
}); });
ipcMain.on("trayIcon", async (event) => { ipcMain.on("legacyCapturer", (event) => {
event.returnValue = await getConfig("trayIcon"); event.returnValue = getConfig("useLegacyCapturer");
}); });
ipcMain.on("disableAutogain", async (event) => { ipcMain.on("trayIcon", (event) => {
event.returnValue = await getConfig("disableAutogain"); event.returnValue = getConfig("trayIcon");
});
ipcMain.on("disableAutogain", (event) => {
event.returnValue = getConfig("disableAutogain");
}); });
ipcMain.on("titlebar", (event) => { ipcMain.on("titlebar", (event) => {
event.returnValue = customTitlebar; event.returnValue = customTitlebar;
}); });
ipcMain.on("mobileMode", async (event) => { ipcMain.on("mobileMode", (event) => {
event.returnValue = await getConfig("mobileMode"); 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", () => { ipcMain.on("openSettingsWindow", () => {
createSettingsWindow(); void createSettingsWindow();
}); });
ipcMain.on("openManagerWindow", () => { ipcMain.on("openManagerWindow", () => {
createTManagerWindow(); void createTManagerWindow();
}); });
ipcMain.on("setting-armcordCSP", async (event) => { ipcMain.on("setting-armcordCSP", (event) => {
if (await getConfig("armcordCSP")) { if (getConfig("armcordCSP")) {
event.returnValue = true; event.returnValue = true;
} else { } else {
event.returnValue = false; 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) => { ipcMain.on("saveSettings", (_event, args: Settings) => {
console.log(args); console.log(args);
setConfigBulk(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); shell.showItemInFolder(storagePath);
await sleep(1000);
}); });
ipcMain.on("openThemesFolder", async () => { ipcMain.on("openThemesFolder", () => {
shell.showItemInFolder(themesPath); shell.showItemInFolder(themesPath);
await sleep(1000);
}); });
ipcMain.on("openPluginsFolder", async () => { ipcMain.on("openPluginsFolder", () => {
shell.showItemInFolder(pluginsPath); shell.showItemInFolder(pluginsPath);
await sleep(1000);
}); });
ipcMain.on("openCrashesFolder", async () => { ipcMain.on("openCrashesFolder", () => {
shell.showItemInFolder(path.join(app.getPath("temp"), `${app.getName()} Crashes`)); shell.showItemInFolder(path.join(app.getPath("temp"), `${app.getName()} Crashes`));
await sleep(1000);
}); });
ipcMain.on("getLangName", async (event) => { ipcMain.on("getLangName", (event) => {
event.returnValue = await getLangName(); event.returnValue = getLangName();
}); });
ipcMain.on("crash", async () => { ipcMain.on("crash", () => {
process.crash(); process.crash();
}); });
ipcMain.handle("getSetting", (_event, toGet: keyof Settings) => { ipcMain.handle("getSetting", (_event, toGet: keyof Settings) => {
return getConfig(toGet); return getConfig(toGet);
}); });
ipcMain.on("copyDebugInfo", () => { ipcMain.on("copyDebugInfo", () => {
let settingsFileContent = fs.readFileSync(getConfigLocation(), "utf-8"); const settingsFileContent = fs.readFileSync(getConfigLocation(), "utf-8");
clipboard.writeText( clipboard.writeText(
`**OS:** ${os.platform()} ${os.version()}\n**Architecture:** ${os.arch()}\n**ArmCord version:** ${getVersion()}\n**Electron version:** ${ `**OS:** ${os.platform()} ${os.version()}\n**Architecture:** ${os.arch()}\n**ArmCord version:** ${getVersion()}\n**Electron version:** ${
process.versions.electron 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 {mainWindow} from "./window.js";
import {createSettingsWindow} from "../settings/main.js"; import {createSettingsWindow} from "../settings/main.js";
function paste(contents: any): void { export function setMenu(): void {
const contentTypes = clipboard.availableFormats().toString(); const template: Electron.MenuItemConstructorOptions[] = [
//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[] = [
{ {
label: "ArmCord", label: "ArmCord",
submenu: [ submenu: [
@ -28,7 +20,7 @@ export async function setMenu(): Promise<void> {
label: "Open settings", label: "Open settings",
accelerator: "CmdOrCtrl+Shift+'", accelerator: "CmdOrCtrl+Shift+'",
click() { click() {
createSettingsWindow(); void createSettingsWindow();
} }
}, },
{ {
@ -67,13 +59,6 @@ export async function setMenu(): Promise<void> {
{type: "separator"}, {type: "separator"},
{label: "Cut", accelerator: "CmdOrCtrl+X", role: "cut"}, {label: "Cut", accelerator: "CmdOrCtrl+X", role: "cut"},
{label: "Copy", accelerator: "CmdOrCtrl+C", role: "copy"}, {label: "Copy", accelerator: "CmdOrCtrl+C", role: "copy"},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
click() {
paste(mainWindow.webContents);
}
},
{label: "Select All", accelerator: "CmdOrCtrl+A", role: "selectAll"} {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 {injectTitlebar} from "./titlebar.mjs";
import type {ArmCordWindow} from "../../types/armcordWindow.d.js";
const CANCEL_ID = "desktop-capturer-selection__cancel"; const CANCEL_ID = "desktop-capturer-selection__cancel";
const desktopCapturer = { const desktopCapturer = {
getSources: (opts: any) => ipcRenderer.invoke("DESKTOP_CAPTURER_GET_SOURCES", opts) getSources: (opts: SourcesOptions) => ipcRenderer.invoke("DESKTOP_CAPTURER_GET_SOURCES", opts)
}; };
interface IPCSources { interface IPCSources {
id: string; id: string;
@ -10,9 +11,9 @@ interface IPCSources {
thumbnail: HTMLCanvasElement; thumbnail: HTMLCanvasElement;
} }
async function getDisplayMediaSelector(): Promise<string> { async function getDisplayMediaSelector(): Promise<string> {
const sources: IPCSources[] = await desktopCapturer.getSources({ const sources = (await desktopCapturer.getSources({
types: ["screen", "window"] types: ["screen", "window"]
}); })) as IPCSources[];
return `<div class="desktop-capturer-selection__scroller"> return `<div class="desktop-capturer-selection__scroller">
<ul class="desktop-capturer-selection__list"> <ul class="desktop-capturer-selection__list">
${sources ${sources
@ -44,24 +45,25 @@ contextBridge.exposeInMainWorld("armcord", {
}, },
titlebar: { titlebar: {
injectTitlebar: () => injectTitlebar(), injectTitlebar: () => injectTitlebar(),
isTitlebar: ipcRenderer.sendSync("titlebar") isTitlebar: ipcRenderer.sendSync("titlebar") as boolean
}, },
electron: process.versions.electron, electron: process.versions.electron,
channel: ipcRenderer.sendSync("channel"), channel: ipcRenderer.sendSync("channel") as string,
setPingCount: (pingCount: number) => ipcRenderer.send("setPing", pingCount), setPingCount: (pingCount: number) => ipcRenderer.send("setPing", pingCount),
setTrayIcon: (favicon: string) => ipcRenderer.send("sendTrayIcon", favicon), setTrayIcon: (favicon: string) => ipcRenderer.send("sendTrayIcon", favicon),
getLang: (toGet: string) => getLang: (toGet: string) =>
ipcRenderer.invoke("getLang", toGet).then((result) => { ipcRenderer.invoke("getLang", toGet).then((result) => {
return result; return result as string;
}), }),
getDisplayMediaSelector, getDisplayMediaSelector,
version: ipcRenderer.sendSync("get-app-version", "app-version"), version: ipcRenderer.sendSync("get-app-version", "app-version") as string,
mods: ipcRenderer.sendSync("clientmod"), mods: ipcRenderer.sendSync("clientmod") as string,
openSettingsWindow: () => ipcRenderer.send("openSettingsWindow") openSettingsWindow: () => ipcRenderer.send("openSettingsWindow")
}); } as ArmCordWindow);
let windowCallback: (arg0: object) => void; let windowCallback: (arg0: object) => void;
contextBridge.exposeInMainWorld("ArmCordRPC", { contextBridge.exposeInMainWorld("ArmCordRPC", {
listen: (callback: any) => { // REVIEW - I don't think this is right
listen: (callback: () => void) => {
windowCallback = callback; windowCallback = callback;
} }
}); });

View file

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

View file

@ -1,9 +1,8 @@
import * as path from "path"; import path from "path";
import * as fs from "fs"; import fs from "fs";
import {addStyle} from "../../common/dom.js"; import {addStyle} from "../../common/dom.js";
import {WebviewTag} from "electron";
var webview = `<webview src="${path.join( const webview = `<webview src="${path.join(
"file://", "file://",
import.meta.dirname, import.meta.dirname,
"../", "../",
@ -25,8 +24,8 @@ export function injectSettings() {
document.addEventListener("DOMContentLoaded", function (_event) { document.addEventListener("DOMContentLoaded", function (_event) {
const settingsCssPath = path.join(import.meta.dirname, "../", "/content/css/inAppSettings.css"); const settingsCssPath = path.join(import.meta.dirname, "../", "/content/css/inAppSettings.css");
addStyle(fs.readFileSync(settingsCssPath, "utf8")); addStyle(fs.readFileSync(settingsCssPath, "utf8"));
const webview = document.querySelector("webview") as WebviewTag; const webview = document.querySelector("webview")!;
webview.addEventListener("console-message", (e) => { 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 {ipcRenderer} from "electron";
import {addStyle} from "../../common/dom.js"; import {addStyle} from "../../common/dom.js";
import * as fs from "fs"; import fs from "fs";
import * as path from "path"; import path from "path";
import os from "os"; import os from "os";
export function injectTitlebar(): void { export function injectTitlebar(): void {
document.addEventListener("DOMContentLoaded", function (_event) { 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." 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) { if (response == 0) {
return true; return true;
} else { } else {
@ -23,48 +23,51 @@ function showAudioDialog(): boolean {
} }
function registerCustomHandler(): void { function registerCustomHandler(): void {
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
console.log(request); console.log(request);
const sources = await desktopCapturer.getSources({ void desktopCapturer
types: ["screen", "window"] .getSources({
}); types: ["screen", "window"]
console.log(sources); })
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") { .then((sources) => {
console.log("WebRTC Capturer detected, skipping window creation."); //assume webrtc capturer is used console.log(sources);
var options: Electron.Streams = {video: sources[0]}; if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") {
if (showAudioDialog() == true) options = {video: sources[0], audio: "loopbackWithMute"}; console.log("WebRTC Capturer detected, skipping window creation."); //assume webrtc capturer is used
callback(options); let options: Electron.Streams = {video: sources[0]};
} else {
capturerWindow = new BrowserWindow({
width: 800,
height: 600,
title: "ArmCord Screenshare",
darkTheme: true,
icon: iconPath,
frame: true,
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
spellcheck: false,
preload: path.join(import.meta.dirname, "preload.js")
}
});
ipcMain.once("selectScreenshareSource", (_event, id, name) => {
//console.log(sources[id]);
//console.log(id);
capturerWindow.close();
let result = {id, name};
if (process.platform === "linux" || process.platform === "win32") {
var options: Electron.Streams = {video: sources[0]};
if (showAudioDialog() == true) options = {video: sources[0], audio: "loopbackWithMute"}; if (showAudioDialog() == true) options = {video: sources[0], audio: "loopbackWithMute"};
callback(options); callback(options);
} else { } else {
callback({video: result}); capturerWindow = new BrowserWindow({
width: 800,
height: 600,
title: "ArmCord Screenshare",
darkTheme: true,
icon: iconPath,
frame: true,
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
spellcheck: false,
preload: path.join(import.meta.dirname, "preload.mjs")
}
});
ipcMain.once("selectScreenshareSource", (_event, id: string, name: string) => {
//console.log(sources[id]);
//console.log(id);
capturerWindow.close();
const result = {id, name};
if (process.platform === "linux" || process.platform === "win32") {
let options: Electron.Streams = {video: sources[0]};
if (showAudioDialog() == true) options = {video: sources[0], audio: "loopbackWithMute"};
callback(options);
} else {
callback({video: result});
}
});
void capturerWindow.loadURL(`file://${import.meta.dirname}/picker.html`);
capturerWindow.webContents.send("getSources", sources);
} }
}); });
capturerWindow.loadURL(`file://${import.meta.dirname}/picker.html`);
capturerWindow.webContents.send("getSources", sources);
}
}); });
} }
registerCustomHandler(); registerCustomHandler();

View file

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

View file

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

View file

@ -1,9 +1,11 @@
// To allow seamless switching between custom titlebar and native os titlebar, // 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 // 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. // 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 {BrowserWindow, MessageBoxOptions, app, dialog, nativeImage, shell} from "electron";
import path from "path"; import path from "path";
import type EventEmitter from "events";
import {ThemeManifest} from "../types/themeManifest.d.js";
import {registerIpc} from "./ipc.js"; import {registerIpc} from "./ipc.js";
import {setMenu} from "./menu.js"; import {setMenu} from "./menu.js";
import * as fs from "fs"; import * as fs from "fs";
@ -28,7 +30,7 @@ contextMenu({
// Only show it when right-clicking text // Only show it when right-clicking text
visible: parameters.selectionText.trim().length > 0, visible: parameters.selectionText.trim().length > 0,
click: () => { 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 // Only show it when right-clicking text
visible: parameters.selectionText.trim().length > 0, visible: parameters.selectionText.trim().length > 0,
click: () => { 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> { function doAfterDefiningTheWindow(): void {
if ((await getWindowState("isMaximized")) ?? false) { if (getWindowState("isMaximized") ?? false) {
mainWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen mainWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen
mainWindow.maximize(); 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 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"); mainWindow.setBackgroundMaterial("mica");
if ((await getConfig("startMinimized")) == false) { if (getConfig("startMinimized") == false) {
mainWindow.show(); 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(); registerIpc();
if (await getConfig("mobileMode")) { if (getConfig("mobileMode")) {
mainWindow.webContents.userAgent = 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"; "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 { } else {
@ -96,9 +101,9 @@ async function doAfterDefiningTheWindow(): Promise<void> {
} }
}; };
if (url.startsWith("https:") || url.startsWith("http:") || url.startsWith("mailto:")) { if (url.startsWith("https:") || url.startsWith("http:") || url.startsWith("mailto:")) {
shell.openExternal(url); void shell.openExternal(url);
} else if (ignoreProtocolWarning) { } else if (ignoreProtocolWarning) {
shell.openExternal(url); void shell.openExternal(url);
} else { } else {
const options: MessageBoxOptions = { const options: MessageBoxOptions = {
type: "question", type: "question",
@ -111,7 +116,7 @@ async function doAfterDefiningTheWindow(): Promise<void> {
checkboxChecked: false checkboxChecked: false
}; };
dialog.showMessageBox(mainWindow, options).then(({response, checkboxChecked}) => { void dialog.showMessageBox(mainWindow, options).then(({response, checkboxChecked}) => {
console.log(response, checkboxChecked); console.log(response, checkboxChecked);
if (checkboxChecked) { if (checkboxChecked) {
if (response == 0) { if (response == 0) {
@ -121,13 +126,13 @@ async function doAfterDefiningTheWindow(): Promise<void> {
} }
} }
if (response == 0) { if (response == 0) {
shell.openExternal(url); void shell.openExternal(url);
} }
}); });
} }
return {action: "deny"}; return {action: "deny"};
}); });
if ((await getConfig("useLegacyCapturer")) == false) { if (getConfig("useLegacyCapturer") == false) {
console.log("Starting screenshare module..."); console.log("Starting screenshare module...");
import("./screenshare/main.js"); import("./screenshare/main.js");
} }
@ -137,9 +142,12 @@ async function doAfterDefiningTheWindow(): Promise<void> {
(_, callback) => callback({cancel: true}) (_, callback) => callback({cancel: true})
); );
if ((await getConfig("trayIcon")) == "default" || (await getConfig("dynamicIcon"))) { if (getConfig("trayIcon") == "default" || getConfig("dynamicIcon")) {
mainWindow.webContents.on("page-favicon-updated", async () => { mainWindow.webContents.on("page-favicon-updated", () => {
let faviconBase64 = await mainWindow.webContents.executeJavaScript(` // REVIEW - no need to await if we just .then() - This works!
void mainWindow.webContents
.executeJavaScript(
`
var getFavicon = function(){ var getFavicon = function(){
var favicon = undefined; var favicon = undefined;
var nodeList = document.getElementsByTagName("link"); var nodeList = document.getElementsByTagName("link");
@ -153,29 +161,33 @@ async function doAfterDefiningTheWindow(): Promise<void> {
return favicon; return favicon;
} }
getFavicon() getFavicon()
`); `
let buf = Buffer.from(faviconBase64.replace(/^data:image\/\w+;base64,/, ""), "base64"); )
fs.writeFileSync(path.join(app.getPath("temp"), "/", "tray.png"), buf, "utf-8"); .then((faviconBase64: string) => {
let trayPath = nativeImage.createFromPath(path.join(app.getPath("temp"), "/", "tray.png")); const buf = Buffer.from(faviconBase64.replace(/^data:image\/\w+;base64,/, ""), "base64");
if (process.platform === "darwin" && trayPath.getSize().height > 22) fs.writeFileSync(path.join(app.getPath("temp"), "/", "tray.png"), buf, "utf-8");
trayPath = trayPath.resize({height: 22}); let trayPath = nativeImage.createFromPath(path.join(app.getPath("temp"), "/", "tray.png"));
if (process.platform === "win32" && trayPath.getSize().height > 32) if (process.platform === "darwin" && trayPath.getSize().height > 22)
trayPath = trayPath.resize({height: 32}); trayPath = trayPath.resize({height: 22});
if (await getConfig("tray")) { if (process.platform === "win32" && trayPath.getSize().height > 32)
if ((await getConfig("trayIcon")) == "default") { trayPath = trayPath.resize({height: 32});
tray.setImage(trayPath); if (getConfig("tray")) {
} if (getConfig("trayIcon") == "default") {
} tray.setImage(trayPath);
if (await getConfig("dynamicIcon")) { }
mainWindow.setIcon(trayPath); }
} 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 */ const armCordSuffix = " - ArmCord"; /* identify */
if (!title.endsWith(armCordSuffix)) { if (!title.endsWith(armCordSuffix)) {
e.preventDefault(); 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}'` `document.title = '${title.replace("Discord |", "") + armCordSuffix}'`
); );
} }
@ -193,7 +205,7 @@ async function doAfterDefiningTheWindow(): Promise<void> {
fs.readdirSync(themesFolder).forEach((file) => { fs.readdirSync(themesFolder).forEach((file) => {
try { try {
const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8"); const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8");
let themeFile = JSON.parse(manifest); const themeFile = JSON.parse(manifest) as ThemeManifest;
if ( if (
fs fs
.readFileSync(path.join(userDataPath, "/disabled.txt")) .readFileSync(path.join(userDataPath, "/disabled.txt"))
@ -213,23 +225,23 @@ async function doAfterDefiningTheWindow(): Promise<void> {
} }
}); });
}); });
await setMenu(); setMenu();
mainWindow.on("close", async (e) => { mainWindow.on("close", (e) => {
if (process.platform === "darwin" && forceQuit) { if (process.platform === "darwin" && forceQuit) {
mainWindow.close(); mainWindow.close();
} else { } else {
let [width, height] = mainWindow.getSize(); const [width, height] = mainWindow.getSize();
await setWindowState({ setWindowState({
width, width,
height, height,
isMaximized: mainWindow.isMaximized(), isMaximized: mainWindow.isMaximized(),
x: mainWindow.getPosition()[0], x: mainWindow.getPosition()[0],
y: mainWindow.getPosition()[1] y: mainWindow.getPosition()[1]
}); });
if (await getConfig("minimizeToTray")) { if (getConfig("minimizeToTray")) {
e.preventDefault(); e.preventDefault();
mainWindow.hide(); mainWindow.hide();
} else if (!(await getConfig("minimizeToTray"))) { } else if (!getConfig("minimizeToTray")) {
e.preventDefault(); e.preventDefault();
app.quit(); app.quit();
} }
@ -244,43 +256,49 @@ async function doAfterDefiningTheWindow(): Promise<void> {
} }
}); });
} }
// REVIEW - Awaiting javascript execution is silly
mainWindow.on("focus", () => { mainWindow.on("focus", () => {
mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`); void mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("unFocused");`);
}); });
mainWindow.on("blur", () => { mainWindow.on("blur", () => {
mainWindow.webContents.executeJavaScript(`document.body.setAttribute("unFocused", "");`); void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("unFocused", "");`);
}); });
mainWindow.on("maximize", () => { mainWindow.on("maximize", () => {
mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`); void mainWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
}); });
mainWindow.on("unmaximize", () => { mainWindow.on("unmaximize", () => {
mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`); void mainWindow.webContents.executeJavaScript(`document.body.removeAttribute("isMaximized");`);
}); });
if ((await getConfig("inviteWebsocket")) == true) { if (getConfig("inviteWebsocket")) {
const server = await new RPCServer(); // NOTE - RPCServer appears to be untyped. cool.
server.on("activity", (data: string) => mainWindow.webContents.send("rpc", data)); // REVIEW - Whatever Ducko has done here to make an async constructor is awful.
server.on("invite", (code: string) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
console.log(code); new RPCServer().then((server: EventEmitter) => {
createInviteWindow(code); server.on("activity", (data: string) => mainWindow.webContents.send("rpc", data));
server.on("invite", (code: string) => {
console.log(code);
createInviteWindow(code);
});
}); });
} }
if (firstRun) { if (firstRun) {
mainWindow.close(); mainWindow.close();
} }
//loadURL broke for no good reason after E28 //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(); mainWindow.show();
} }
} }
export async function createCustomWindow(): Promise<void> { export function createCustomWindow(): void {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: (await getWindowState("width")) ?? 835, width: getWindowState("width") ?? 835,
height: (await getWindowState("height")) ?? 600, height: getWindowState("height") ?? 600,
x: await getWindowState("x"), x: getWindowState("x"),
y: await getWindowState("y"), y: getWindowState("y"),
title: "ArmCord", title: "ArmCord",
show: false, show: false,
darkTheme: true, darkTheme: true,
@ -292,17 +310,17 @@ export async function createCustomWindow(): Promise<void> {
webviewTag: true, webviewTag: true,
sandbox: false, sandbox: false,
preload: path.join(import.meta.dirname, "preload/preload.mjs"), preload: path.join(import.meta.dirname, "preload/preload.mjs"),
spellcheck: await getConfig("spellcheck") spellcheck: getConfig("spellcheck")
} }
}); });
doAfterDefiningTheWindow(); doAfterDefiningTheWindow();
} }
export async function createNativeWindow(): Promise<void> { export function createNativeWindow(): void {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: (await getWindowState("width")) ?? 835, width: getWindowState("width") ?? 835,
height: (await getWindowState("height")) ?? 600, height: getWindowState("height") ?? 600,
x: await getWindowState("x"), x: getWindowState("x"),
y: await getWindowState("y"), y: getWindowState("y"),
title: "ArmCord", title: "ArmCord",
darkTheme: true, darkTheme: true,
icon: iconPath, icon: iconPath,
@ -314,17 +332,17 @@ export async function createNativeWindow(): Promise<void> {
webviewTag: true, webviewTag: true,
sandbox: false, sandbox: false,
preload: path.join(import.meta.dirname, "preload/preload.mjs"), preload: path.join(import.meta.dirname, "preload/preload.mjs"),
spellcheck: await getConfig("spellcheck") spellcheck: getConfig("spellcheck")
} }
}); });
doAfterDefiningTheWindow(); doAfterDefiningTheWindow();
} }
export async function createTransparentWindow(): Promise<void> { export function createTransparentWindow(): void {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: (await getWindowState("width")) ?? 835, width: getWindowState("width") ?? 835,
height: (await getWindowState("height")) ?? 600, height: getWindowState("height") ?? 600,
x: await getWindowState("x"), x: getWindowState("x"),
y: await getWindowState("y"), y: getWindowState("y"),
title: "ArmCord", title: "ArmCord",
darkTheme: true, darkTheme: true,
icon: iconPath, icon: iconPath,
@ -336,12 +354,12 @@ export async function createTransparentWindow(): Promise<void> {
sandbox: false, sandbox: false,
webviewTag: true, webviewTag: true,
preload: path.join(import.meta.dirname, "preload/preload.mjs"), preload: path.join(import.meta.dirname, "preload/preload.mjs"),
spellcheck: await getConfig("spellcheck") spellcheck: getConfig("spellcheck")
} }
}); });
doAfterDefiningTheWindow(); doAfterDefiningTheWindow();
} }
export async function createInviteWindow(code: string): Promise<void> { export function createInviteWindow(code: string): void {
inviteWindow = new BrowserWindow({ inviteWindow = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
@ -352,15 +370,16 @@ export async function createInviteWindow(code: string): Promise<void> {
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
sandbox: false, 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) => { inviteWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => {
if (details.url.includes("ws://")) return callback({cancel: true}); if (details.url.includes("ws://")) return callback({cancel: true});
return callback({}); 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", () => { inviteWindow.webContents.once("did-finish-load", () => {
if (!mainWindow.webContents.isLoading()) { if (!mainWindow.webContents.isLoading()) {
inviteWindow.show(); inviteWindow.show();

View file

@ -11,23 +11,22 @@ import {createSplashWindow} from "./splash/main.js";
import {createSetupWindow} from "./setup/main.js"; import {createSetupWindow} from "./setup/main.js";
import { import {
setConfig, setConfig,
getConfigSync,
checkForDataFolder, checkForDataFolder,
checkIfConfigExists, checkIfConfigExists,
checkIfConfigIsBroken, checkIfConfigIsBroken,
getConfig, getConfig,
firstRun, firstRun,
Settings,
getConfigLocation getConfigLocation
} from "./common/config.js"; } from "./common/config.js";
import {injectElectronFlags} from "./common/flags.js"; import {injectElectronFlags} from "./common/flags.js";
import {setLang} from "./common/lang.js"; import {setLang} from "./common/lang.js";
import {installModLoader} from "./discord/extensions/mods.js"; import {installModLoader} from "./discord/extensions/mods.js";
export let iconPath: string; export let iconPath: string;
export let settings: any; import type {Settings} from "./types/settings";
export let settings: Settings;
export let customTitlebar: boolean; export let customTitlebar: boolean;
app.on("render-process-gone", (event, webContents, details) => { app.on("render-process-gone", (_event, _webContents, details) => {
if (details.reason == "crashed") { if (details.reason == "crashed") {
app.relaunch(); app.relaunch();
} }
@ -35,23 +34,23 @@ app.on("render-process-gone", (event, webContents, details) => {
async function args(): Promise<void> { async function args(): Promise<void> {
let argNum = 2; let argNum = 2;
if (process.argv[0] == "electron") argNum++; if (process.argv[0] == "electron") argNum++;
let args = process.argv[argNum]; const args = process.argv[argNum];
if (args == undefined) return; if (args == undefined) return;
if (args.startsWith("--")) return; //electron flag if (args.startsWith("--")) return; //electron flag
if (args.includes("=")) { if (args.includes("=")) {
let e = args.split("="); const e = args.split("=");
await setConfig(e[0] as keyof Settings, e[1]); setConfig(e[0] as keyof Settings, e[1]);
console.log(`Setting ${e[0]} to ${e[1]}`); console.log(`Setting ${e[0]} to ${e[1]}`);
app.relaunch(); app.relaunch();
app.exit(); app.exit();
} else if (args == "themes") { } else if (args == "themes") {
app.whenReady().then(async () => { await app.whenReady().then(async () => {
createTManagerWindow(); await createTManagerWindow();
}); });
} }
} }
args(); // i want my top level awaits await args(); // i want my top level awaits - IMPLEMENTED :)
if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false ?? undefined)) { if (!app.requestSingleInstanceLock() && getConfig("multiInstance") == (false ?? undefined)) {
// if value isn't set after 3.2.4 // if value isn't set after 3.2.4
// kill if 2nd instance // kill if 2nd instance
app.quit(); app.quit();
@ -82,21 +81,22 @@ if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false
checkIfConfigIsBroken(); checkIfConfigIsBroken();
injectElectronFlags(); injectElectronFlags();
console.log("[Config Manager] Current config: " + fs.readFileSync(getConfigLocation(), "utf-8")); console.log("[Config Manager] Current config: " + fs.readFileSync(getConfigLocation(), "utf-8"));
app.whenReady().then(async () => { void app.whenReady().then(async () => {
if ((await getConfig("customIcon")) !== undefined ?? null) { // REVIEW - Awaiting the line above will cause a hang at startup
iconPath = await getConfig("customIcon"); if (getConfig("customIcon") !== null) {
iconPath = getConfig("customIcon");
} else { } else {
iconPath = path.join(import.meta.dirname, "../", "/assets/desktop.png"); iconPath = path.join(import.meta.dirname, "../", "/assets/desktop.png");
} }
async function init(): Promise<void> { async function init(): Promise<void> {
if ((await getConfig("skipSplash")) == false) { if (getConfig("skipSplash") == false) {
createSplashWindow(); void createSplashWindow(); // REVIEW - Awaiting will hang at start
} }
if (firstRun == true) { if (firstRun == true) {
await setLang(new Intl.DateTimeFormat().resolvedOptions().locale); setLang(new Intl.DateTimeFormat().resolvedOptions().locale);
createSetupWindow(); await createSetupWindow();
} }
switch (await getConfig("windowStyle")) { switch (getConfig("windowStyle")) {
case "default": case "default":
createCustomWindow(); createCustomWindow();
customTitlebar = true; customTitlebar = true;
@ -125,8 +125,9 @@ if (!app.requestSingleInstanceLock() && getConfigSync("multiInstance") == (false
callback(true); callback(true);
} }
}); });
app.on("activate", async function () { app.on("activate", function () {
if (BrowserWindow.getAllWindows().length === 0) await init(); // 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 path from "path";
import fs from "fs"; import fs from "fs";
import {getDisplayVersion} from "../common/version.js"; import {getDisplayVersion} from "../common/version.js";
import type {ThemeManifest} from "../types/themeManifest.d.js";
let settingsWindow: BrowserWindow; let settingsWindow: BrowserWindow;
let instance = 0; let instance = 0;
export function createSettingsWindow(): void { export async function createSettingsWindow(): Promise<void> {
console.log("Creating a settings window."); console.log("Creating a settings window.");
instance += 1; instance += 1;
if (instance > 1) { if (instance > 1) {
@ -28,7 +29,7 @@ export function createSettingsWindow(): void {
} }
}); });
async function settingsLoadPage(): Promise<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 userDataPath = app.getPath("userData");
const themesFolder = `${userDataPath}/themes/`; const themesFolder = `${userDataPath}/themes/`;
@ -43,7 +44,7 @@ export function createSettingsWindow(): void {
fs.readdirSync(themesFolder).forEach((file) => { fs.readdirSync(themesFolder).forEach((file) => {
try { try {
const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8"); const manifest = fs.readFileSync(`${themesFolder}/${file}/manifest.json`, "utf8");
let themeFile = JSON.parse(manifest); const themeFile = JSON.parse(manifest) as ThemeManifest;
if ( if (
fs fs
.readFileSync(path.join(userDataPath, "/disabled.txt")) .readFileSync(path.join(userDataPath, "/disabled.txt"))
@ -64,10 +65,10 @@ export function createSettingsWindow(): void {
}); });
}); });
settingsWindow.webContents.setWindowOpenHandler(({url}) => { settingsWindow.webContents.setWindowOpenHandler(({url}) => {
shell.openExternal(url); void shell.openExternal(url);
return {action: "deny"}; return {action: "deny"};
}); });
settingsLoadPage(); await settingsLoadPage();
settingsWindow.on("close", () => { settingsWindow.on("close", () => {
instance = 0; instance = 0;
}); });

View file

@ -1,11 +1,14 @@
import {contextBridge, ipcRenderer} from "electron"; import {contextBridge, ipcRenderer} from "electron";
import {Settings} from "../types/settings";
//import {addStyle} from "../utils.js"; //import {addStyle} from "../utils.js";
console.log("ArmCord Settings"); console.log("ArmCord Settings");
console.log(process.platform); console.log(process.platform);
contextBridge.exposeInMainWorld("settings", { 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"), 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), getLang: (toGet: string) => ipcRenderer.invoke("getLang", toGet),
get: (toGet: string) => ipcRenderer.invoke("getSetting", toGet), get: (toGet: string) => ipcRenderer.invoke("getSetting", toGet),
openThemesFolder: () => ipcRenderer.send("openThemesFolder"), openThemesFolder: () => ipcRenderer.send("openThemesFolder"),
@ -17,7 +20,8 @@ contextBridge.exposeInMainWorld("settings", {
crash: () => ipcRenderer.send("crash"), crash: () => ipcRenderer.send("crash"),
os: process.platform os: process.platform
}); });
/*
ipcRenderer.on("themeLoader", (_event, message) => { ipcRenderer.on("themeLoader", (_event, message) => {
//addStyle(message); //addStyle(message);
}); });
*/

View file

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

View file

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

View file

@ -1,42 +1,47 @@
import {BrowserWindow, app, ipcMain} from "electron"; import {BrowserWindow, app, ipcMain} from "electron";
import path from "path"; import path from "path";
import * as fs from "fs"; import fs from "fs";
import {iconPath} from "../main.js"; 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; let setupWindow: BrowserWindow;
export function createSetupWindow(): void { export async function createSetupWindow(): Promise<void> {
setupWindow = new BrowserWindow({ // NOTE - intentionally hang the process until setup is completed
width: 390, return new Promise((resolve) => {
height: 470, setupWindow = new BrowserWindow({
title: "ArmCord Setup", width: 390,
darkTheme: true, height: 470,
icon: iconPath, title: "ArmCord Setup",
frame: false, darkTheme: true,
autoHideMenuBar: true, icon: iconPath,
webPreferences: { frame: false,
sandbox: false, autoHideMenuBar: true,
spellcheck: false, webPreferences: {
preload: path.join(import.meta.dirname, "preload.mjs") sandbox: false,
} spellcheck: false,
}); preload: path.join(import.meta.dirname, "preload.mjs")
ipcMain.on("saveSettings", (_event, args: Settings) => { }
console.log(args);
setConfigBulk(args);
});
ipcMain.on("setup-minimize", () => {
setupWindow.minimize();
});
ipcMain.on("setup-getOS", (event) => {
event.returnValue = process.platform;
});
ipcMain.on("setup-quit", async () => {
fs.unlink(await getConfigLocation(), (err) => {
if (err) throw err;
console.log('Closed during setup. "settings.json" was deleted');
app.quit();
}); });
ipcMain.on("saveSettings", (_event, args: Settings) => {
console.log(args);
setConfigBulk(args);
resolve();
});
ipcMain.on("setup-minimize", () => {
setupWindow.minimize();
});
ipcMain.on("setup-getOS", (event) => {
event.returnValue = process.platform;
});
ipcMain.on("setup-quit", () => {
fs.unlink(getConfigLocation(), (err) => {
if (err) throw err;
console.log('Closed during setup. "settings.json" was deleted');
app.quit();
});
});
void setupWindow.loadURL(`file://${import.meta.dirname}/setup.html`);
}); });
setupWindow.loadURL(`file://${import.meta.dirname}/setup.html`);
} }

View file

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

View file

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -123,9 +123,8 @@
}); });
if (window.armcordinternal.getOS == "linux") { if (window.armcordinternal.getOS == "linux") {
document.getElementById("tray").value = "false"; document.getElementById("tray").value = "false";
document.getElementById( document.getElementById("linuxNotice").innerHTML =
"linuxNotice" `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.`;
).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", () => { document.getElementById("next-page4").addEventListener("click", () => {
window.armcordinternal.saveSettings({ window.armcordinternal.saveSettings({

View file

@ -19,5 +19,5 @@ export async function createSplashWindow(): Promise<void> {
preload: path.join(import.meta.dirname, "preload.mjs") 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", { contextBridge.exposeInMainWorld("internal", {
restart: () => ipcRenderer.send("restart"), restart: () => ipcRenderer.send("restart"),
installState: ipcRenderer.sendSync("modInstallState"), installState: ipcRenderer.sendSync("modInstallState") as string,
version: ipcRenderer.sendSync("get-app-version", "app-version"), version: ipcRenderer.sendSync("get-app-version", "app-version") as string,
getLang: (toGet: string) => getLang: (toGet: string) =>
ipcRenderer.invoke("getLang", toGet).then((result) => { ipcRenderer.invoke("getLang", toGet).then((result: string) => {
return result; return result;
}), }),
splashEnd: () => ipcRenderer.send("splashEnd") splashEnd: () => ipcRenderer.send("splashEnd")

View file

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

View file

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

View file

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

View file

@ -1,36 +1,22 @@
import {BrowserWindow, app, dialog, ipcMain, shell} from "electron"; import {BrowserWindow, app, dialog, ipcMain, shell} from "electron";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import {sleep} from "../common/sleep.js";
import {createInviteWindow, mainWindow} from "../discord/window.js"; import {createInviteWindow, mainWindow} from "../discord/window.js";
import type {ThemeManifest} from "../types/themeManifest.d.js";
let themeWindow: BrowserWindow; let themeWindow: BrowserWindow;
let instance = 0; 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) { function parseBDManifest(content: string) {
const metaReg = /@([^ ]*) (.*)/g; const metaReg = /@([^ ]*) (.*)/g;
if (!content.startsWith("/**")) { if (!content.startsWith("/**")) {
throw new Error("Not a manifest."); 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; let match;
while ((match = metaReg.exec(content)) !== null) { while ((match = metaReg.exec(content)) !== null) {
let [_, key, value] = match; const [_, key] = match;
let [value] = match;
if (key === "import") break; if (key === "import") break;
value = value.trim(); value = value.trim();
@ -88,7 +74,7 @@ function parseBDManifest(content: string) {
} }
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const themesPath = path.join(userDataPath, "/themes/"); const themesPath = path.join(userDataPath, "/themes/");
export function createTManagerWindow(): void { export async function createTManagerWindow(): Promise<void> {
console.log("Creating theme manager window."); console.log("Creating theme manager window.");
instance += 1; instance += 1;
if (instance > 1) { if (instance > 1) {
@ -118,13 +104,13 @@ export function createTManagerWindow(): void {
if (url.startsWith("https://discord.gg/")) { if (url.startsWith("https://discord.gg/")) {
createInviteWindow(url.replace("https://discord.gg/", "")); createInviteWindow(url.replace("https://discord.gg/", ""));
} else { } else {
shell.openExternal(url); void shell.openExternal(url);
} }
} }
}); });
async function managerLoadPage(): Promise<void> { 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 userDataPath = app.getPath("userData");
const themesFolder = `${userDataPath}/themes/`; const themesFolder = `${userDataPath}/themes/`;
@ -135,27 +121,24 @@ export function createTManagerWindow(): void {
if (!fs.existsSync(`${userDataPath}/disabled.txt`)) { if (!fs.existsSync(`${userDataPath}/disabled.txt`)) {
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), ""); fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), "");
} }
ipcMain.on("openThemesFolder", async () => { ipcMain.on("openThemesFolder", () => {
shell.showItemInFolder(themesPath); shell.showItemInFolder(themesPath);
await sleep(1000);
}); });
ipcMain.on("reloadMain", async () => { ipcMain.on("reloadMain", () => {
mainWindow.webContents.reload(); 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`); 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(); e.returnValue = fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
}); });
ipcMain.on("removeFromDisabled", async (_event, name: string) => { ipcMain.on("removeFromDisabled", (_event, name: string) => {
let e = await fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString(); const e = fs.readFileSync(path.join(userDataPath, "/disabled.txt")).toString();
fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), e.replace(name, "")); fs.writeFileSync(path.join(userDataPath, "/disabled.txt"), e.replace(name, ""));
sleep(1000);
}); });
ipcMain.on("uninstallTheme", async (_event, id: string) => { ipcMain.on("uninstallTheme", (_event, id: string) => {
let themePath = path.join(themesFolder, id); const themePath = path.join(themesFolder, id);
if (fs.existsSync(themePath)) { if (fs.existsSync(themePath)) {
fs.rmdirSync(themePath, {recursive: true}); fs.rmdirSync(themePath, {recursive: true});
console.log(`Removed ${id} folder`); console.log(`Removed ${id} folder`);
@ -166,33 +149,35 @@ export function createTManagerWindow(): void {
themeWindow.webContents.reload(); themeWindow.webContents.reload();
mainWindow.webContents.reload(); mainWindow.webContents.reload();
}); });
ipcMain.on("installBDTheme", async (_event, link: string) => { ipcMain.on("installBDTheme", (_event, link: string) => {
try { return async () => {
let code = await (await fetch(link)).text(); try {
let manifest = parseBDManifest(code); const code = await (await fetch(link)).text();
let themePath = path.join(themesFolder, `${manifest.name?.replace(" ", "-")}-BD`); const manifest = parseBDManifest(code);
if (!fs.existsSync(themePath)) { const themePath = path.join(themesFolder, `${manifest.name?.replace(" ", "-")}-BD`);
fs.mkdirSync(themePath); if (!fs.existsSync(themePath)) {
console.log(`Created ${manifest.name} folder`); fs.mkdirSync(themePath);
console.log(`Created ${manifest.name} folder`);
}
manifest.updateSrc = link;
if (code.includes(".titlebar")) manifest.supportsArmCordTitlebar = true;
else manifest.supportsArmCordTitlebar = false;
fs.writeFileSync(path.join(themePath, "manifest.json"), JSON.stringify(manifest));
fs.writeFileSync(path.join(themePath, "src.css"), code);
dialog.showMessageBoxSync({
type: "info",
title: "BD Theme import success",
message: "Successfully imported theme from link."
});
themeWindow.webContents.reload();
mainWindow.webContents.reload();
} catch (e) {
dialog.showErrorBox(
"BD Theme import fail",
"Failed to import theme from link. Please make sure that it's a valid BetterDiscord Theme."
);
} }
manifest.updateSrc = link; };
if (code.includes(".titlebar")) manifest.supportsArmCordTitlebar = true;
else manifest.supportsArmCordTitlebar = false;
fs.writeFileSync(path.join(themePath, "manifest.json"), JSON.stringify(manifest));
fs.writeFileSync(path.join(themePath, "src.css"), code);
dialog.showMessageBoxSync({
type: "info",
title: "BD Theme import success",
message: "Successfully imported theme from link."
});
themeWindow.webContents.reload();
mainWindow.webContents.reload();
} catch (e) {
dialog.showErrorBox(
"BD Theme import fail",
"Failed to import theme from link. Please make sure that it's a valid BetterDiscord Theme."
);
}
}); });
themeWindow.webContents.on("did-finish-load", () => { themeWindow.webContents.on("did-finish-load", () => {
fs.readdirSync(themesFolder).forEach((file) => { fs.readdirSync(themesFolder).forEach((file) => {
@ -206,7 +191,7 @@ export function createTManagerWindow(): void {
}); });
}); });
managerLoadPage(); await managerLoadPage();
themeWindow.on("close", () => { themeWindow.on("close", () => {
instance = 0; instance = 0;
}); });

View file

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<style> <style>
@ -213,7 +213,9 @@
border-style: solid; border-style: solid;
border-radius: 10px; border-radius: 10px;
width: 80%; 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-name: animatetop;
-webkit-animation-duration: 0.4s; -webkit-animation-duration: 0.4s;
animation-name: animatetop; animation-name: animatetop;

View file

@ -1,18 +1,20 @@
import {ipcRenderer, contextBridge} from "electron"; import {ipcRenderer, contextBridge} from "electron";
import {sleep} from "../common/sleep.js"; import {sleep} from "../common/sleep.js";
import {ThemeManifest} from "../types/themeManifest";
contextBridge.exposeInMainWorld("themes", { contextBridge.exposeInMainWorld("themes", {
install: (url: string) => ipcRenderer.send("installBDTheme", url), install: (url: string) => ipcRenderer.send("installBDTheme", url),
uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id) uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id)
}); });
ipcRenderer.on("themeManifest", (_event, json) => { ipcRenderer.on("themeManifest", (_event, json: string) => {
let manifest = JSON.parse(json); async () => {
console.log(manifest); const manifest = JSON.parse(json) as ThemeManifest;
sleep(1000); console.log(manifest);
let e = document.getElementById("cardBox"); await sleep(1000); // REVIEW - This is all that requires async, would be nice if it could be removed.
let id = manifest.name.replace(" ", "-"); const e = document.getElementById("cardBox");
e?.insertAdjacentHTML( const id = manifest.name.replace(" ", "-");
"beforeend", e?.insertAdjacentHTML(
` "beforeend",
`
<div class="card"> <div class="card">
<div class="flex-box"> <div class="flex-box">
<h3 id="${`${id}header`}">${manifest.name}</h3> <h3 id="${`${id}header`}">${manifest.name}</h3>
@ -22,50 +24,47 @@ ipcRenderer.on("themeManifest", (_event, json) => {
<p>${manifest.description}</p> <p>${manifest.description}</p>
</div> </div>
` `
); );
document.getElementById(`${id}header`)!.addEventListener("click", () => { document.getElementById(`${id}header`)!.addEventListener("click", () => {
document.getElementById("themeInfoModal")!.style.display = "block"; document.getElementById("themeInfoModal")!.style.display = "block";
document.getElementById("themeInfoName")!.textContent = `${manifest.name} by ${manifest.author}`; document.getElementById("themeInfoName")!.textContent = `${manifest.name} by ${manifest.author}`;
document.getElementById("themeInfoDesc")!.textContent = `${manifest.description}\n\n${manifest.version}`; document.getElementById("themeInfoDesc")!.textContent = `${manifest.description}\n\n${manifest.version}`;
if (manifest.supportsArmCordTitlebar !== undefined) { if (manifest.supportsArmCordTitlebar !== undefined) {
document.getElementById( document.getElementById("themeInfoButtons")!.innerHTML +=
"themeInfoButtons" `<img class="themeInfoIcon" id="removeTheme" onclick="themes.uninstall('${id}')" title="Remove the theme" src="https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/Trash.png"></img>
)!.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="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>`; <img class="themeInfoIcon" id="compatibility" title="Supports ArmCord Titlebar" src=""></img>`;
console.log("e"); console.log("e");
if (manifest.supportsArmCordTitlebar == true) { if (manifest.supportsArmCordTitlebar == true) {
(document.getElementById(`compatibility`) as HTMLImageElement).src = (document.getElementById(`compatibility`) as HTMLImageElement).src =
"https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/Window.png"; "https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/Window.png";
} else { } else {
(document.getElementById(`compatibility`) as HTMLImageElement).src = (document.getElementById(`compatibility`) as HTMLImageElement).src =
"https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/WindowUnsupported.png"; "https://raw.githubusercontent.com/ArmCord/BrandingStuff/main/WindowUnsupported.png";
}
} }
if (manifest.source != undefined)
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>`;
if (manifest.invite != undefined)
document.getElementById("themeInfoButtons")!.innerHTML +=
`<a href="${`https://discord.gg/${manifest.invite}`}" class="button">Support Discord</a>`;
});
if (!(ipcRenderer.sendSync("disabled") as string[]).includes(id)) {
(document.getElementById(id) as HTMLInputElement).checked = true;
} }
if (manifest.source != undefined) (document.getElementById(id) as HTMLInputElement).addEventListener("input", function () {
document.getElementById( ipcRenderer.send("reloadMain");
"themeInfoButtons" if (this.checked) {
)!.innerHTML += `<a href="${manifest.source}" class="button">Source code</a>`; ipcRenderer.send("removeFromDisabled", id);
if (manifest.website != undefined) } else {
document.getElementById( ipcRenderer.send("addToDisabled", id);
"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>`;
});
if (!ipcRenderer.sendSync("disabled").includes(id)) {
(document.getElementById(id) as HTMLInputElement).checked = true;
}
(document.getElementById(id) as HTMLInputElement)!.addEventListener("input", function (evt) {
ipcRenderer.send("reloadMain");
if (this.checked) {
ipcRenderer.send("removeFromDisabled", id);
} else {
ipcRenderer.send("addToDisabled", id);
}
});
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.getElementsByClassName("close")[0].addEventListener("click", () => { document.getElementsByClassName("close")[0].addEventListener("click", () => {
@ -73,6 +72,6 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("themeInfoButtons")!.innerHTML = ""; document.getElementById("themeInfoButtons")!.innerHTML = "";
}); });
document.getElementById("download")!.addEventListener("click", () => { 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 {Menu, MessageBoxOptions, Tray, app, dialog, nativeImage} from "electron";
import {createInviteWindow, mainWindow} from "./discord/window.js"; import {createInviteWindow, mainWindow} from "./discord/window.js";
import * as path from "path"; import path from "path";
import {createSettingsWindow} from "./settings/main.js"; import {createSettingsWindow} from "./settings/main.js";
import {getConfig, getConfigLocation, setConfig} from "./common/config.js"; import {getConfig, getConfigLocation, setConfig} from "./common/config.js";
import {getDisplayVersion} from "./common/version.js"; import {getDisplayVersion} from "./common/version.js";
export let tray: any = null; export let tray: Tray;
let trayIcon = "ac_plug_colored"; let trayIcon = "ac_plug_colored";
app.whenReady().then(async () => { void app.whenReady().then(async () => {
let finishedSetup = await getConfig("doneSetup"); // REVIEW - app will hang at startup if line above is awaited.
if ((await getConfig("trayIcon")) != "default") { const finishedSetup = getConfig("doneSetup");
trayIcon = await getConfig("trayIcon"); if (getConfig("trayIcon") != "default") {
trayIcon = getConfig("trayIcon");
} }
let trayPath = nativeImage.createFromPath(path.join(import.meta.dirname, "../", `/assets/${trayIcon}.png`)); let trayPath = nativeImage.createFromPath(path.join(import.meta.dirname, "../", `/assets/${trayIcon}.png`));
let trayVerIcon; const trayVerIcon = function () {
trayVerIcon = function () {
if (process.platform == "win32") { if (process.platform == "win32") {
return trayPath.resize({height: 16}); return trayPath.resize({height: 16});
} else if (process.platform == "darwin") { } 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 (process.platform == "darwin" && trayPath.getSize().height > 22) trayPath = trayPath.resize({height: 22});
if (await getConfig("tray")) { if (getConfig("tray")) {
let clientName = (await getConfig("clientName")) ?? "ArmCord"; const clientName = getConfig("clientName") ?? "ArmCord";
tray = new Tray(trayPath); tray = new Tray(trayPath);
if (finishedSetup == false) { if (finishedSetup == false) {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
@ -37,8 +37,8 @@ app.whenReady().then(async () => {
}, },
{ {
label: `Quit ${clientName}`, label: `Quit ${clientName}`,
async click() { click() {
fs.unlink(await getConfigLocation(), (err) => { fs.unlink(getConfigLocation(), (err) => {
if (err) throw err; if (err) throw err;
console.log('Closed during setup. "settings.json" was deleted'); console.log('Closed during setup. "settings.json" was deleted');
@ -50,6 +50,7 @@ app.whenReady().then(async () => {
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
} else { } else {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
// REVIEW - Awaiting any window creation will fail silently
{ {
label: `${clientName} ${getDisplayVersion()}`, label: `${clientName} ${getDisplayVersion()}`,
icon: trayVerIcon(), icon: trayVerIcon(),
@ -67,13 +68,13 @@ app.whenReady().then(async () => {
{ {
label: "Open Settings", label: "Open Settings",
click() { click() {
createSettingsWindow(); void createSettingsWindow();
} }
}, },
{ {
label: "Support Discord Server", label: "Support Discord Server",
click() { click() {
createInviteWindow("TnhxcqynZ2"); void createInviteWindow("TnhxcqynZ2");
} }
}, },
{ {
@ -82,7 +83,8 @@ app.whenReady().then(async () => {
{ {
label: `Quit ${clientName}`, label: `Quit ${clientName}`,
click() { click() {
app.quit(); // NOTE - Temporary fix for app not actually quitting
app.exit();
} }
} }
]); ]);
@ -93,7 +95,7 @@ app.whenReady().then(async () => {
mainWindow.show(); mainWindow.show();
}); });
} else { } else {
if ((await getConfig("tray")) == undefined) { if (getConfig("tray") == undefined) {
if (process.platform == "linux") { if (process.platform == "linux") {
const options: MessageBoxOptions = { const options: MessageBoxOptions = {
type: "question", 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." 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) { if (response == 0) {
setConfig("tray", true); setConfig("tray", true);
} else { } 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 // 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. //"exclude": ["src/**/*.test.ts"], // Exclude .test.ts files since they're for Jest only.
"compilerOptions": { "compilerOptions": {
// Project Structure // // 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. "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". "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. "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. "noUnusedLocals": true, // Warns you if you have unused variables.
// Output // // Output //
"module": "ES2022", // Compiles ES6 imports to require() syntax. "module": "ESNext", // Compiles ES6 imports to require() syntax.
"removeComments": false, "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. "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. "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. //"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 // // 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. "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). "importHelpers": false // Reduce the amount of bloat that comes from downlevelIteration (when polyfills are redeclared).
} }