24 Commits

Author SHA1 Message Date
oxmc
d9c4a4a428 Improve app and add more install options (and fix old .dmg files) 2024-12-28 01:08:07 -08:00
oxmc
77dcea14cc Add userStyles support (wip) 2024-12-27 14:01:09 -08:00
oxmc
dcbbfde4a5 Reformat build dir 2024-12-20 09:28:49 -08:00
oxmc
685c2dc210 Fix sha256 method in aur build 2024-12-20 08:48:23 -08:00
oxmc
3d67f6d5ae change sha256 file paths 2024-12-20 08:36:27 -08:00
oxmc
e4ca9882b9 fix macOS sha256 2024-12-20 08:26:41 -08:00
oxmc
9c4a886799 Add extension support and work on auto update 2024-12-20 08:22:49 -08:00
oxmc
6839090631 Merge pull request #12 from GizzyUwU/main
remove a.yml
2024-12-20 05:50:07 -08:00
Gizzy
1232eea297 remove a.yml 2024-12-20 13:21:39 +00:00
oxmc
b88509d210 Merge pull request #10 from PlOszukiwaczDEV/main
fixed yq
2024-12-20 05:14:17 -08:00
oxmc
2f95583af1 Merge pull request #11 from GizzyUwU/main
Fixed version change on aur publish
2024-12-20 05:14:08 -08:00
Gizzy
6f6ab38c9f test 2024-12-20 13:12:39 +00:00
PlOszukiwacz
10887b8358 yq revert 2024-12-20 14:01:43 +01:00
PlOszukiwacz
4ce43bdac3 yq test 2 2024-12-20 13:57:38 +01:00
Gizzy
9947bfc3dc test 2024-12-20 12:52:53 +00:00
PlOszukiwacz
b68a85ac2f yq test 1 2024-12-20 13:50:31 +01:00
oxmc
dfbb0492ac Merge pull request #9 from PlOszukiwaczDEV/main
Added appimagelint to check the appimages
2024-12-20 04:46:50 -08:00
PlOszukiwacz
def3cd312d libfuse dosent exist ig 2024-12-20 13:45:49 +01:00
Gizzy
5539ae2c63 test 2024-12-20 12:43:44 +00:00
PlOszukiwacz
53bf782ddc appimagelint test 3 (libfuse) 2024-12-20 13:43:35 +01:00
Gizzy
01e9154a33 test 2024-12-20 12:37:30 +00:00
PlOszukiwacz
04ec30066d appimagelint test 2 2024-12-20 13:36:57 +01:00
PlOszukiwacz
8a1b2ac65f appimagelint test 1 2024-12-20 13:33:28 +01:00
Gizzy
cb0cfbe640 test 2024-12-20 11:37:36 +00:00
31 changed files with 2355 additions and 229 deletions

View File

@@ -31,12 +31,30 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Build (ia32)
run: npm run build -- --arch ia32
- name: Build (x64) - name: Build (x64)
run: npm run build -- --arch x64 run: npm run build -- --arch x64
- name: Build (arm64) - name: Build (arm64)
run: npm run build -- --arch arm64 run: npm run build -- --arch arm64
- name: Download appimagelint and its deps
run: |
sudo apt update && sudo apt install fuse -y
wget https://github.com/TheAssassin/appimagelint/releases/download/continuous/appimagelint-x86_64.AppImage
chmod +x appimagelint-x86_64.AppImage
- name: Check the appimage(s)
run: ./appimagelint-x86_64.AppImage dist/*.AppImage
- name: Generate checksum
run: |
sha256sum dist/*.AppImage > dist/sha256sum.txt
sha256sum dist/*.deb >> dist/sha256sum.txt
sha256sum dist/*.zip >> dist/sha256sum.txt
- name: Upload Linux Artifacts - name: Upload Linux Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
id: upload-artifact id: upload-artifact
@@ -44,23 +62,15 @@ jobs:
name: linux-artifacts name: linux-artifacts
path: | path: |
dist/*.AppImage dist/*.AppImage
dist/*.deb
dist/*.zip
dist/latest*.yml dist/latest*.yml
dist/sha256sum.txt
- name: Generate checksum
run: |
sha256sum dist/*.AppImage > sha256sum.txt
- name: Upload checksums
uses: actions/upload-artifact@v4
with:
name: checksums
path: sha256sum.txt
build-windows: build-windows:
name: Build bsky-desktop (Windows) name: Build bsky-desktop (Windows)
runs-on: windows-latest runs-on: windows-latest
env: env:
ext: "exe"
GITHUB_TOKEN: ${{ secrets.GHT }} GITHUB_TOKEN: ${{ secrets.GHT }}
steps: steps:
@@ -75,12 +85,22 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Build (ia32)
run: npm run build -- --arch ia32
- name: Build (x64) - name: Build (x64)
run: npm run build -- --arch x64 run: npm run build -- --arch x64
- name: Build (arm64) - name: Build (arm64)
run: npm run build -- --arch arm64 run: npm run build -- --arch arm64
- name: Generate checksum
run: |
sha256sum dist/*.exe > dist/sha256sum.txt
sha256sum dist/*.msi >> dist/sha256sum.txt
sha256sum dist/*.appx >> dist/sha256sum.txt
sha256sum dist/*.zip >> dist/sha256sum.txt
- name: Upload Windows Artifacts - name: Upload Windows Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
id: upload-artifact id: upload-artifact
@@ -88,13 +108,16 @@ jobs:
name: windows-artifacts name: windows-artifacts
path: | path: |
dist/*.exe dist/*.exe
dist/*.msi
dist/*.appx
dist/*.zip
dist/latest*.yml dist/latest*.yml
dist/sha256sum.txt
build-macos: build-macos:
name: Build bsky-desktop (macOS) name: Build bsky-desktop (macOS)
runs-on: macos-latest runs-on: macos-latest
env: env:
ext: "dmg"
GITHUB_TOKEN: ${{ secrets.GHT }} GITHUB_TOKEN: ${{ secrets.GHT }}
steps: steps:
@@ -115,6 +138,12 @@ jobs:
- name: Build (arm64) - name: Build (arm64)
run: npm run build -- --arch arm64 run: npm run build -- --arch arm64
- name: Generate checksum
run: |
shasum -a 256 dist/*.dmg > dist/sha256sum.txt
shasum -a 256 dist/*.pkg >> dist/sha256sum.txt
shasum -a 256 dist/*.zip >> dist/sha256sum.txt
- name: Upload macOS Artifacts - name: Upload macOS Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
id: upload-artifact id: upload-artifact
@@ -122,7 +151,10 @@ jobs:
name: macos-artifacts name: macos-artifacts
path: | path: |
dist/*.dmg dist/*.dmg
dist/*.pkg
dist/*.zip
dist/latest*.yml dist/latest*.yml
dist/sha256sum.txt
release: release:
name: Create Release name: Create Release
@@ -162,10 +194,9 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -R dist run: ls -R dist
- name: Merge latest .ymls - name: Combine checksums
uses: mikefarah/yq@v4.44.6 run: |
with: cat dist/linux/sha256sum.txt dist/windows/sha256sum.txt dist/macos/sha256sum.txt > sha256sums.txt
cmd: yq eval-all '. as $item ireduce ({}; . * $item )' dist/*/*.yml > merged.yml
- name: Upload Release - name: Upload Release
id: create_release id: create_release
@@ -176,9 +207,16 @@ jobs:
generate_release_notes: true generate_release_notes: true
files: | files: |
dist/linux/*.AppImage dist/linux/*.AppImage
dist/linux/*.deb
dist/linux/*.zip
dist/windows/*.exe dist/windows/*.exe
dist/windows/*.msi
dist/windows/*.appx
dist/windows/*.zip
dist/macos/*.dmg dist/macos/*.dmg
merged.yml dist/macos/*.pkg
dist/macos/*.zip
sha256sums.txt
aur: aur:
name: Publish to AUR name: Publish to AUR
@@ -191,33 +229,33 @@ jobs:
- name: Checkout git repo - name: Checkout git repo
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install jq - name: Download linux artifacts
run: sudo apt-get update && sudo apt-get install jq -y
- name: Download checksums
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: checksums name: linux-artifacts
path: . path: dist/linux
- name: List downloaded files - name: List downloaded files
run: ls -R run: ls -R dist
- name: Show content of sha256sum.txt - name: Show content of sha256sum.txt
run: cat sha256sum.txt run: cat dist/linux/sha256sum.txt
- name: Get app version
id: version
uses: pchynoweth/action-get-npm-version@1.1.1
- name: Extract checksum from sha256sum.txt and change build version - name: Extract checksum from sha256sum.txt and change build version
run: | run: |
echo releasever new_checksum=$(awk 'NR==1 { print $1 }' ./dist/linux/sha256sum.txt)
new_checksum=$(awk 'NR==1 { print $1 }' ./sha256sum.txt) sed -i "s|sha256sums=('SKIP' 'SKIP')|sha256sums=('$new_checksum' 'SKIP')|" ./build/arch-pkg/PKGBUILD
sed -i "s|sha256sums=('SKIP' 'SKIP')|sha256sums=('$new_checksum' 'SKIP')|" ./build/PKGBUILD sed -i "s/^pkgver=.*$/pkgver=${{ steps.version.outputs.version }}/" ./build/arch-pkg/PKGBUILD
sed -i "s/^pkgver=.*$/pkgver=${{ needs.release.outputs.version_tag }}/" ./build/PKGBUILD
- name: Publish AUR package - name: Publish AUR package
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1 uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
with: with:
pkgname: bskydesktop pkgname: bskydesktop
pkgbuild: ./build/PKGBUILD pkgbuild: ./build/arch-pkg/PKGBUILD
commit_username: ${{ secrets.AUR_USERNAME }} commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }} commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}

View File

@@ -1,3 +1,58 @@
# Bsky Desktop # Bsky Desktop
Bsky Desktop is an application for Bsky built using Electron. It allows users to manage their Bsky account and feeds from the app instead of the web. Bsky Desktop is an Electron-based application for Bsky that allows users to manage their accounts and feeds directly from the app, rather than through the web interface.
### Features:
- Support for user styles (work in progress; currently only LESS preprocessor is supported)
- Compatibility with both Manifest V2 and V3 Chrome extensions, though only a limited set of Chrome extension APIs are supported. For more information, visit: [Electron Extensions API Documentation](https://www.electronjs.org/docs/latest/api/extensions#supported-extensions-apis)
### Working on:
- Auto updates (for all platforms)
### Build and release status:
[![Build and Release bsky-desktop](https://github.com/oxmc/bsky-desktop/actions/workflows/build-and-release.yml/badge.svg)](https://github.com/oxmc/bsky-desktop/actions/workflows/build-and-release.yml)
[![Packaging status](https://repology.org/badge/vertical-allrepos/bskydesktop.svg?columns=4&exclude_unsupported=1)](https://repology.org/project/bskydesktop/versions)
#### Windows install options:
- Zip (x64, arm64, ia32)
- Setup (exe, msi, appx) (x64, arm64, ia32)
#### Mac install options:
- Zip (x64, arm64)
- Dmg (x64, arm64)
- Pkg (x64, arm64)
#### Linux install options:
- Zip (x64, arm64, ia32)
- AppImage (x64, arm64, ia32)
- Deb (x64, arm64, ia32)
### Build Instructions for Bsky Desktop
To build and run Bsky Desktop locally, follow these steps:
1. **Clone the repository:**
```sh
git clone https://github.com/oxmc/bsky-desktop.git
cd bsky-desktop
```
2. **Install dependencies:**
```sh
npm install
```
**(Optional) Run the application locally:**
If you want to test the application locally before building it, use the following command:
```sh
npm run start
```
This step is **not required for building** but is useful if you want to see the app in action during development.
3. **Build the application:**
To compile the application, run:
```sh
npm run build
```
This will generate the necessary files for the app.

View File

@@ -1,116 +0,0 @@
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs/promises');
const process = require('process');
const packageJson = require('./package.json');
// Electron builder:
const electronBuilderPath = path.join('node_modules', 'electron-builder', 'cli.js');
// Parse command-line arguments:
const supportedPlatforms = ['win', 'mac', 'linux', 'mwl'];
const supportedArchitectures = ['x64', 'armv7l', 'arm64', 'ia32', 'universal'];
const args = process.argv.slice(2);
const carch = args.includes('--arch') ? args[args.indexOf('--arch') + 1] : null;
const cplatform = args.includes('--platform') ? args[args.indexOf('--platform') + 1] : null;
const pack = args.includes('--pack') || null;
let platform, arch, build_args;
//console.log(supportedPlatforms, supportedArchitectures, cplatform, carch);
// Platform Name:
if (cplatform == null) {
switch (process.platform) {
case "win32":
platform = "win";
break;
case "darwin":
platform = "mac";
break;
case "linux":
platform = "linux";
break;
default:
platform = "mwl"; // Build for all
break;
}
} else {
if (!supportedPlatforms.includes(cplatform)) {
console.error(`Invalid platform specified. Supported platforms: ${supportedPlatforms.join(', ')}`);
process.exit(1);
}
platform = cplatform;
}
// Platform Arch:
if (carch == null) {
arch = process.arch === "arm" ? "armv7l" : process.arch;
} else {
arch = carch;
if (!supportedArchitectures.includes(arch)) {
console.error(`Invalid arch specified. Supported architectures: ${supportedArchitectures.join(', ')}`);
process.exit(1);
}
}
// Generate artifact name:
const artifactname = `${packageJson.build.productName}-${packageJson.version}-${platform}-${arch}`;
(async () => {
try {
// Additional build arguments: (started wiht --eb-nmehere)
const additionalArgs = args.filter((arg, index) => arg.startsWith('--eb-') && index % 2 !== 0);
// CLI Args:
build_args = [
electronBuilderPath,
`--${platform}`,
`--${arch}`,
`-c.artifactName="${artifactname}.\${ext}"`,
];
// If additional args are present:
if (additionalArgs.length > 0) {
build_args.push(...additionalArgs);
}
// If pack is true:
if (pack) {
build_args.push(`--dir`);
}
// Make CLI:
const cli = `node ${build_args.join(' ')}`;
console.info(`CLI: ${cli}`);
console.info(`Building ${artifactname}`);
const process = spawn(cli, { shell: true });
// Log stdout as it comes
process.stdout.on('data', (data) => {
console.log(data.toString().trim());
});
// Log stderr as it comes
process.stderr.on('data', (data) => {
console.error(data.toString().trim());
});
// Handle process exit
process.on('close', (code) => {
if (code === 0) {
console.info(`Build completed successfully!`);
} else {
console.error(`Process exited with code ${code}`);
}
});
process.on('error', (error) => {
console.error(`Failed to start process: ${error.message}`);
});
} catch (error) {
console.error('An error occurred:', error);
process.exit(1);
}
})();

View File

@@ -16,14 +16,17 @@ icon_name="bsky-desktop.png"
prepare() { prepare() {
latest_tag=$(curl -s "https://api.github.com/repos/oxmc/bsky-desktop/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")') latest_tag=$(curl -s "https://api.github.com/repos/oxmc/bsky-desktop/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")')
latest_sha256=$(curl -Ls "https://github.com/oxmc/bsky-desktop/releases/download/$latest_tag/sha256sums.txt" | grep "AppImage")
echo "Latest release tag: $latest_tag" echo "Latest release tag: $latest_tag"
case "$CARCH" in case "$CARCH" in
x86_64) x86_64)
appimage_name="bskyDesktop-${latest_tag:1}-linux-x64.AppImage" appimage_name="bskyDesktop-${latest_tag:1}-linux-x64.AppImage"
sha256sum=$(echo "$latest_sha256" | grep "x64" | cut -d' ' -f1)
;; ;;
aarch64) aarch64)
appimage_name="bskyDesktop-${latest_tag:1}-linux-arm64.AppImage" appimage_name="bskyDesktop-${latest_tag:1}-linux-arm64.AppImage"
sha256sum=$(echo "$latest_sha256" | grep "arm64" | cut -d' ' -f1)
;; ;;
*) *)
echo "Unsupported architecture: $CARCH" echo "Unsupported architecture: $CARCH"
@@ -36,7 +39,7 @@ prepare() {
"$icon_url" "$icon_url"
) )
echo "AppImage source: ${source[0]}" echo "AppImage source: ${source[0]}"
sha256sums=('SKIP' 'SKIP') sha256sums=("$sha256sum" 'SKIP')
curl -L "${source[0]}" -o "$srcdir/bskyDesktop.appimage" curl -L "${source[0]}" -o "$srcdir/bskyDesktop.appimage"
curl -L "${source[1]}" -o "$srcdir/$icon_name" curl -L "${source[1]}" -o "$srcdir/$icon_name"
} }
@@ -70,7 +73,7 @@ package() {
[Desktop Entry] [Desktop Entry]
Name=Bluesky Desktop Name=Bluesky Desktop
Comment=Bluesky Desktop Client Comment=Bluesky Desktop Client
Exec=/usr/bin/bsky-desktop Exec=/usr/bin/bsky-desktop %u
Icon=bsky-desktop Icon=bsky-desktop
Terminal=false Terminal=false
Type=Application Type=Application

104
build/build-app.js Normal file
View File

@@ -0,0 +1,104 @@
const path = require('path');
const process = require('process');
const builder = require('electron-builder');
const { Platform, Arch } = builder;
// Parse command-line arguments
const supportedPlatforms = ['win', 'mac', 'linux'];
const supportedArchitectures = ['x64', 'armv7l', 'arm64', 'ia32', 'universal'];
const args = process.argv.slice(2);
const carch = args.includes('--arch') ? args[args.indexOf('--arch') + 1] : null;
const cplatform = args.includes('--platform') ? args[args.indexOf('--platform') + 1] : null;
const pack = args.includes('--pack'); // Keep track of --pack as a boolean flag
// Determine platform
let platform;
if (!cplatform) {
platform = process.platform === 'win32' ? 'win' :
process.platform === 'darwin' ? 'mac' :
process.platform === 'linux' ? 'linux' : null;
} else if (!supportedPlatforms.includes(cplatform)) {
console.error(`Invalid platform specified. Supported platforms: ${supportedPlatforms.join(', ')}`);
process.exit(1);
} else {
platform = cplatform;
}
// Determine architecture
let arch = carch || (process.arch === 'arm' ? 'armv7l' : process.arch);
if (!supportedArchitectures.includes(arch)) {
console.error(`Invalid architecture specified. Supported architectures: ${supportedArchitectures.join(', ')}`);
process.exit(1);
}
// Map platform and architecture to electron-builder enums
const platformEnum = {
win: Platform.WINDOWS,
mac: Platform.MAC,
linux: Platform.LINUX
}[platform];
const archEnum = {
x64: Arch.x64,
armv7l: Arch.armv7l,
arm64: Arch.arm64,
ia32: Arch.ia32,
universal: Arch.universal
}[arch];
// Additional build arguments: (starting with --eb-)
const buildArgs = args.filter((arg) => arg.startsWith('--eb-'));
// If additional args are present, add them to the buildArgs array
if (buildArgs.length > 0) {
console.log('Additional build arguments:', buildArgs);
}
// If pack is true, add --dir to buildArgs
if (pack) {
buildArgs.push('--dir');
}
// Read build-config.json
const buildConfigPath = path.join(__dirname, 'build-config.json');
let buildConfig;
try {
buildConfig = require(buildConfigPath);
} catch (err) {
console.error(`Failed to read build-config.json: ${err.message}`);
process.exit(1);
}
// Generate artifact name
const packageJson = require('../package.json');
const artifactName = `${buildConfig.productName}-${packageJson.version}-${platform}-${arch}.\${ext}`;
// Electron Builder configuration
/**
* @type {import('electron-builder').Configuration}
*/
const options = {
artifactName,
nsis: {
deleteAppDataOnUninstall: true,
oneClick: true,
perMachine: false
},
dmg: {
contents: [
{ type: 'file', x: 255, y: 85 },
{ type: 'link', path: '/Applications', x: 253, y: 325 }
]
},
...buildConfig
};
// Build process
builder.build({
targets: platformEnum.createTarget(null, archEnum),
config: options
}).then(result => {
console.log("Build completed:", JSON.stringify(result, null, 2));
}).catch(error => {
console.error("Build failed:", error);
});

55
build/build-config.json Normal file
View File

@@ -0,0 +1,55 @@
{
"appId": "com.oxmc.bskyDesktop",
"productName": "bskyDesktop",
"asarUnpack": [
"./node_modules/node-notifier/**/*"
],
"removePackageScripts": true,
"mac": {
"target": [
"dmg",
"pkg",
"zip"
],
"icon": "src/ui/images/mac.icns",
"category": "Network"
},
"pkg": {
"installLocation": "/Applications",
"allowAnywhere": true,
"allowCurrentUserHome": true,
"allowRootDirectory": true,
"isVersionChecked": true,
"isRelocatable": false,
"overwriteAction": "upgrade"
},
"linux": {
"target": [
"appimage",
"deb",
"zip"
],
"icon": "src/ui/images/icons",
"category": "Network"
},
"win": {
"target": [
"nsis",
"appx",
"msi",
"zip"
],
"icon": "src/ui/images/logo.ico"
},
"appx": {
"identityName": "BskyDesktop"
},
"protocols": [
{
"name": "bsky",
"schemes": [
"bsky"
]
}
]
}

295
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "bsky-desktop", "name": "bsky-desktop",
"version": "1.0.6", "version": "1.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bsky-desktop", "name": "bsky-desktop",
"version": "1.0.6", "version": "1.1.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.17", "@electron/asar": "^3.2.17",
@@ -14,9 +14,11 @@
"adm-zip": "^0.5.12", "adm-zip": "^0.5.12",
"axios": "^1.6.8", "axios": "^1.6.8",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"less": "^4.2.1",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"node-notifier": "^10.0.0", "node-notifier": "^10.0.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"usercss-meta": "^0.12.0",
"v8-compile-cache": "^2.3.0" "v8-compile-cache": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
@@ -1324,6 +1326,18 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"license": "MIT",
"dependencies": {
"is-what": "^3.14.1"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -1711,6 +1725,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"license": "MIT",
"optional": true,
"dependencies": {
"prr": "~1.0.1"
},
"bin": {
"errno": "cli.js"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -2265,7 +2292,7 @@
}, },
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -2295,6 +2322,19 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"license": "MIT",
"optional": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"license": "ISC", "license": "ISC",
@@ -2343,6 +2383,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
"license": "MIT"
},
"node_modules/is-wsl": { "node_modules/is-wsl": {
"version": "2.2.0", "version": "2.2.0",
"license": "MIT", "license": "MIT",
@@ -2522,6 +2568,45 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/less": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/less/-/less-4.2.1.tgz",
"integrity": "sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==",
"license": "Apache-2.0",
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
"tslib": "^2.3.0"
},
"bin": {
"lessc": "bin/lessc"
},
"engines": {
"node": ">=6"
},
"optionalDependencies": {
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
}
},
"node_modules/less/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2604,6 +2689,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"license": "MIT",
"optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/matcher": { "node_modules/matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -2723,6 +2832,23 @@
"version": "2.1.2", "version": "2.1.2",
"license": "MIT" "license": "MIT"
}, },
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "1.7.2", "version": "1.7.2",
"dev": true, "dev": true,
@@ -2797,6 +2923,15 @@
"dev": true, "dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"license": "MIT", "license": "MIT",
@@ -2842,6 +2977,16 @@
"version": "1.2.0", "version": "1.2.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/plist": { "node_modules/plist": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
@@ -2894,6 +3039,13 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"license": "MIT",
"optional": true
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@@ -3063,7 +3215,7 @@
}, },
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sanitize-filename": { "node_modules/sanitize-filename": {
@@ -3080,7 +3232,7 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"dev": true, "devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/semver": { "node_modules/semver": {
@@ -3189,7 +3341,7 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true, "devOptional": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -3466,6 +3618,12 @@
"utf8-byte-length": "^1.0.1" "utf8-byte-length": "^1.0.1"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
@@ -3517,6 +3675,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/usercss-meta": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/usercss-meta/-/usercss-meta-0.12.0.tgz",
"integrity": "sha512-zKrXCKdpeIwtVe87omxGo9URf+7mbozduMZEg79dmT4KB3XJwfIkEi/Uk0PcTwR/nZLtAK1+k7isgbGB/g6E7Q==",
"license": "MIT",
"engines": {
"node": ">=8.3"
}
},
"node_modules/utf8-byte-length": { "node_modules/utf8-byte-length": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
@@ -4647,6 +4814,14 @@
} }
} }
}, },
"copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"requires": {
"is-what": "^3.14.1"
}
},
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -4914,6 +5089,15 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true "dev": true
}, },
"errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"optional": true,
"requires": {
"prr": "~1.0.1"
}
},
"es-define-property": { "es-define-property": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -5289,7 +5473,7 @@
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"dev": true, "devOptional": true,
"requires": { "requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
} }
@@ -5300,6 +5484,12 @@
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true "dev": true
}, },
"image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"optional": true
},
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"requires": { "requires": {
@@ -5328,6 +5518,11 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true "dev": true
}, },
"is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA=="
},
"is-wsl": { "is-wsl": {
"version": "2.2.0", "version": "2.2.0",
"requires": { "requires": {
@@ -5462,6 +5657,31 @@
} }
} }
}, },
"less": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/less/-/less-4.2.1.tgz",
"integrity": "sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==",
"requires": {
"copy-anything": "^2.0.1",
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"parse-node-version": "^1.0.1",
"source-map": "~0.6.0",
"tslib": "^2.3.0"
},
"dependencies": {
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"optional": true
}
}
},
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -5527,6 +5747,24 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"optional": true,
"requires": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"optional": true
}
}
},
"matcher": { "matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -5605,6 +5843,16 @@
"ms": { "ms": {
"version": "2.1.2" "version": "2.1.2"
}, },
"needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"optional": true,
"requires": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
}
},
"node-addon-api": { "node-addon-api": {
"version": "1.7.2", "version": "1.7.2",
"dev": true, "dev": true,
@@ -5656,6 +5904,11 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true "dev": true
}, },
"parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
},
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1" "version": "1.0.1"
}, },
@@ -5686,6 +5939,12 @@
"pend": { "pend": {
"version": "1.2.0" "version": "1.2.0"
}, },
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"optional": true
},
"plist": { "plist": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
@@ -5724,6 +5983,12 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
}, },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"optional": true
},
"pump": { "pump": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@@ -5851,7 +6116,7 @@
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"dev": true "devOptional": true
}, },
"sanitize-filename": { "sanitize-filename": {
"version": "1.6.3", "version": "1.6.3",
@@ -5866,7 +6131,7 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"dev": true "devOptional": true
}, },
"semver": { "semver": {
"version": "7.6.3", "version": "7.6.3",
@@ -5938,7 +6203,7 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "devOptional": true
}, },
"source-map-support": { "source-map-support": {
"version": "0.5.21", "version": "0.5.21",
@@ -6135,6 +6400,11 @@
"utf8-byte-length": "^1.0.1" "utf8-byte-length": "^1.0.1"
} }
}, },
"tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"type-fest": { "type-fest": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
@@ -6167,6 +6437,11 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"usercss-meta": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/usercss-meta/-/usercss-meta-0.12.0.tgz",
"integrity": "sha512-zKrXCKdpeIwtVe87omxGo9URf+7mbozduMZEg79dmT4KB3XJwfIkEi/Uk0PcTwR/nZLtAK1+k7isgbGB/g6E7Q=="
},
"utf8-byte-length": { "utf8-byte-length": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "bsky-desktop", "name": "bsky-desktop",
"version": "1.0.8", "version": "1.1.1",
"description": "A desktop app of bsky.app", "description": "A desktop app of bsky.app",
"main": "src/app/main.js", "main": "src/app/main.js",
"scripts": { "scripts": {
@@ -8,7 +8,7 @@
"pack": "electron-builder --dir", "pack": "electron-builder --dir",
"dist": "electron-builder", "dist": "electron-builder",
"rebuild": "electron-rebuild", "rebuild": "electron-rebuild",
"build": "node ./build-app.js" "build": "node ./build/build-app.js"
}, },
"author": { "author": {
"name": "oxmc", "name": "oxmc",
@@ -25,46 +25,11 @@
"adm-zip": "^0.5.12", "adm-zip": "^0.5.12",
"axios": "^1.6.8", "axios": "^1.6.8",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"less": "^4.2.1",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"node-notifier": "^10.0.0", "node-notifier": "^10.0.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"usercss-meta": "^0.12.0",
"v8-compile-cache": "^2.3.0" "v8-compile-cache": "^2.3.0"
},
"build": {
"appId": "com.oxmc.bskyDesktop",
"productName": "bskyDesktop",
"asarUnpack": [
"./node_modules/node-notifier/**/*"
],
"artifactName": "bsky-desktop.${ext}",
"mac": {
"target": [
"dmg",
"pkg"
],
"icon": "src/ui/images/logo.icns",
"category": "Network"
},
"linux": {
"target": [
"appimage"
],
"icon": "src/ui/images/icons",
"category": "Network"
},
"win": {
"target": [
"nsis"
],
"icon": "src/ui/images/logo.ico"
},
"protocols": [
{
"name": "bsky",
"schemes": [
"bsky"
]
}
]
} }
} }

17
src/app/contributors.json Normal file
View File

@@ -0,0 +1,17 @@
[
{
"name": "oxmc",
"email": "contact@oxmc.is-a.dev",
"url": "https://oxmc.is-a.dev"
},
{
"name": "PlOszukiwaczDEV",
"email": "ploszukiwacz1@duck.com",
"url": "https://ploszukiwacz.pl"
},
{
"name": "GizzyUwU",
"email": "me@gizzy.pro",
"url": "https://gizzy.pro"
}
]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 devsnek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,660 @@
'use strict';
const EventEmitter = require('events');
const { setTimeout, clearTimeout } = require('timers');
const fetch = require('node-fetch');
const transports = require('./transports');
const { RPCCommands, RPCEvents, RelationshipTypes } = require('./constants');
const { pid: getPid, uuid } = require('./util');
function subKey(event, args) {
return `${event}${JSON.stringify(args)}`;
}
/**
* @typedef {RPCClientOptions}
* @extends {ClientOptions}
* @prop {string} transport RPC transport. one of `ipc` or `websocket`
*/
/**
* The main hub for interacting with Discord RPC
* @extends {BaseClient}
*/
class RPCClient extends EventEmitter {
/**
* @param {RPCClientOptions} [options] Options for the client.
* You must provide a transport
*/
constructor(options = {}) {
super();
this.options = options;
this.accessToken = null;
this.clientId = null;
/**
* Application used in this client
* @type {?ClientApplication}
*/
this.application = null;
/**
* User used in this application
* @type {?User}
*/
this.user = null;
const Transport = transports[options.transport];
if (!Transport) {
throw new TypeError('RPC_INVALID_TRANSPORT', options.transport);
}
this.fetch = (method, path, { data, query } = {}) =>
fetch(`${this.fetch.endpoint}${path}${query ? new URLSearchParams(query) : ''}`, {
method,
body: data,
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
}).then(async (r) => {
const body = await r.json();
if (!r.ok) {
const e = new Error(r.status);
e.body = body;
throw e;
}
return body;
});
this.fetch.endpoint = 'https://discord.com/api';
/**
* Raw transport userd
* @type {RPCTransport}
* @private
*/
this.transport = new Transport(this);
this.transport.on('message', this._onRpcMessage.bind(this));
/**
* Map of nonces being expected from the transport
* @type {Map}
* @private
*/
this._expecting = new Map();
this._connectPromise = undefined;
}
/**
* Search and connect to RPC
*/
connect(clientId) {
if (this._connectPromise) {
return this._connectPromise;
}
this._connectPromise = new Promise((resolve, reject) => {
this.clientId = clientId;
const timeout = setTimeout(() => reject(new Error('RPC_CONNECTION_TIMEOUT')), 10e3);
timeout.unref();
this.once('connected', () => {
clearTimeout(timeout);
resolve(this);
});
this.transport.once('close', () => {
this._expecting.forEach((e) => {
e.reject(new Error('connection closed'));
});
this.emit('disconnected');
reject(new Error('connection closed'));
});
this.transport.connect().catch(reject);
});
return this._connectPromise;
}
/**
* @typedef {RPCLoginOptions}
* @param {string} clientId Client ID
* @param {string} [clientSecret] Client secret
* @param {string} [accessToken] Access token
* @param {string} [rpcToken] RPC token
* @param {string} [tokenEndpoint] Token endpoint
* @param {string[]} [scopes] Scopes to authorize with
*/
/**
* Performs authentication flow. Automatically calls Client#connect if needed.
* @param {RPCLoginOptions} options Options for authentication.
* At least one property must be provided to perform login.
* @example client.login({ clientId: '1234567', clientSecret: 'abcdef123' });
* @returns {Promise<RPCClient>}
*/
async login(options = {}) {
let { clientId, accessToken } = options;
await this.connect(clientId);
if (!options.scopes) {
this.emit('ready');
return this;
}
if (!accessToken) {
accessToken = await this.authorize(options);
}
return this.authenticate(accessToken);
}
/**
* Request
* @param {string} cmd Command
* @param {Object} [args={}] Arguments
* @param {string} [evt] Event
* @returns {Promise}
* @private
*/
request(cmd, args, evt) {
return new Promise((resolve, reject) => {
const nonce = uuid();
this.transport.send({ cmd, args, evt, nonce });
this._expecting.set(nonce, { resolve, reject });
});
}
/**
* Message handler
* @param {Object} message message
* @private
*/
_onRpcMessage(message) {
if (message.cmd === RPCCommands.DISPATCH && message.evt === RPCEvents.READY) {
if (message.data.user) {
this.user = message.data.user;
}
this.emit('connected');
} else if (this._expecting.has(message.nonce)) {
const { resolve, reject } = this._expecting.get(message.nonce);
if (message.evt === 'ERROR') {
const e = new Error(message.data.message);
e.code = message.data.code;
e.data = message.data;
reject(e);
} else {
resolve(message.data);
}
this._expecting.delete(message.nonce);
} else {
this.emit(message.evt, message.data);
}
}
/**
* Authorize
* @param {Object} options options
* @returns {Promise}
* @private
*/
async authorize({ scopes, clientSecret, rpcToken, redirectUri, prompt } = {}) {
if (clientSecret && rpcToken === true) {
const body = await this.fetch('POST', '/oauth2/token/rpc', {
data: new URLSearchParams({
client_id: this.clientId,
client_secret: clientSecret,
}),
});
rpcToken = body.rpc_token;
}
const { code } = await this.request('AUTHORIZE', {
scopes,
client_id: this.clientId,
prompt,
rpc_token: rpcToken,
});
const response = await this.fetch('POST', '/oauth2/token', {
data: new URLSearchParams({
client_id: this.clientId,
client_secret: clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
}),
});
return response.access_token;
}
/**
* Authenticate
* @param {string} accessToken access token
* @returns {Promise}
* @private
*/
authenticate(accessToken) {
return this.request('AUTHENTICATE', { access_token: accessToken })
.then(({ application, user }) => {
this.accessToken = accessToken;
this.application = application;
this.user = user;
this.emit('ready');
return this;
});
}
/**
* Fetch a guild
* @param {Snowflake} id Guild ID
* @param {number} [timeout] Timeout request
* @returns {Promise<Guild>}
*/
getGuild(id, timeout) {
return this.request(RPCCommands.GET_GUILD, { guild_id: id, timeout });
}
/**
* Fetch all guilds
* @param {number} [timeout] Timeout request
* @returns {Promise<Collection<Snowflake, Guild>>}
*/
getGuilds(timeout) {
return this.request(RPCCommands.GET_GUILDS, { timeout });
}
/**
* Get a channel
* @param {Snowflake} id Channel ID
* @param {number} [timeout] Timeout request
* @returns {Promise<Channel>}
*/
getChannel(id, timeout) {
return this.request(RPCCommands.GET_CHANNEL, { channel_id: id, timeout });
}
/**
* Get all channels
* @param {Snowflake} [id] Guild ID
* @param {number} [timeout] Timeout request
* @returns {Promise<Collection<Snowflake, Channel>>}
*/
async getChannels(id, timeout) {
const { channels } = await this.request(RPCCommands.GET_CHANNELS, {
timeout,
guild_id: id,
});
return channels;
}
/**
* @typedef {CertifiedDevice}
* @prop {string} type One of `AUDIO_INPUT`, `AUDIO_OUTPUT`, `VIDEO_INPUT`
* @prop {string} uuid This device's Windows UUID
* @prop {object} vendor Vendor information
* @prop {string} vendor.name Vendor's name
* @prop {string} vendor.url Vendor's url
* @prop {object} model Model information
* @prop {string} model.name Model's name
* @prop {string} model.url Model's url
* @prop {string[]} related Array of related product's Windows UUIDs
* @prop {boolean} echoCancellation If the device has echo cancellation
* @prop {boolean} noiseSuppression If the device has noise suppression
* @prop {boolean} automaticGainControl If the device has automatic gain control
* @prop {boolean} hardwareMute If the device has a hardware mute
*/
/**
* Tell discord which devices are certified
* @param {CertifiedDevice[]} devices Certified devices to send to discord
* @returns {Promise}
*/
setCertifiedDevices(devices) {
return this.request(RPCCommands.SET_CERTIFIED_DEVICES, {
devices: devices.map((d) => ({
type: d.type,
id: d.uuid,
vendor: d.vendor,
model: d.model,
related: d.related,
echo_cancellation: d.echoCancellation,
noise_suppression: d.noiseSuppression,
automatic_gain_control: d.automaticGainControl,
hardware_mute: d.hardwareMute,
})),
});
}
/**
* @typedef {UserVoiceSettings}
* @prop {Snowflake} id ID of the user these settings apply to
* @prop {?Object} [pan] Pan settings, an object with `left` and `right` set between
* 0.0 and 1.0, inclusive
* @prop {?number} [volume=100] The volume
* @prop {bool} [mute] If the user is muted
*/
/**
* Set the voice settings for a user, by id
* @param {Snowflake} id ID of the user to set
* @param {UserVoiceSettings} settings Settings
* @returns {Promise}
*/
setUserVoiceSettings(id, settings) {
return this.request(RPCCommands.SET_USER_VOICE_SETTINGS, {
user_id: id,
pan: settings.pan,
mute: settings.mute,
volume: settings.volume,
});
}
/**
* Move the user to a voice channel
* @param {Snowflake} id ID of the voice channel
* @param {Object} [options] Options
* @param {number} [options.timeout] Timeout for the command
* @param {boolean} [options.force] Force this move. This should only be done if you
* have explicit permission from the user.
* @returns {Promise}
*/
selectVoiceChannel(id, { timeout, force = false } = {}) {
return this.request(RPCCommands.SELECT_VOICE_CHANNEL, { channel_id: id, timeout, force });
}
/**
* Move the user to a text channel
* @param {Snowflake} id ID of the voice channel
* @param {Object} [options] Options
* @param {number} [options.timeout] Timeout for the command
* have explicit permission from the user.
* @returns {Promise}
*/
selectTextChannel(id, { timeout } = {}) {
return this.request(RPCCommands.SELECT_TEXT_CHANNEL, { channel_id: id, timeout });
}
/**
* Get current voice settings
* @returns {Promise}
*/
getVoiceSettings() {
return this.request(RPCCommands.GET_VOICE_SETTINGS)
.then((s) => ({
automaticGainControl: s.automatic_gain_control,
echoCancellation: s.echo_cancellation,
noiseSuppression: s.noise_suppression,
qos: s.qos,
silenceWarning: s.silence_warning,
deaf: s.deaf,
mute: s.mute,
input: {
availableDevices: s.input.available_devices,
device: s.input.device_id,
volume: s.input.volume,
},
output: {
availableDevices: s.output.available_devices,
device: s.output.device_id,
volume: s.output.volume,
},
mode: {
type: s.mode.type,
autoThreshold: s.mode.auto_threshold,
threshold: s.mode.threshold,
shortcut: s.mode.shortcut,
delay: s.mode.delay,
},
}));
}
/**
* Set current voice settings, overriding the current settings until this session disconnects.
* This also locks the settings for any other rpc sessions which may be connected.
* @param {Object} args Settings
* @returns {Promise}
*/
setVoiceSettings(args) {
return this.request(RPCCommands.SET_VOICE_SETTINGS, {
automatic_gain_control: args.automaticGainControl,
echo_cancellation: args.echoCancellation,
noise_suppression: args.noiseSuppression,
qos: args.qos,
silence_warning: args.silenceWarning,
deaf: args.deaf,
mute: args.mute,
input: args.input ? {
device_id: args.input.device,
volume: args.input.volume,
} : undefined,
output: args.output ? {
device_id: args.output.device,
volume: args.output.volume,
} : undefined,
mode: args.mode ? {
type: args.mode.type,
auto_threshold: args.mode.autoThreshold,
threshold: args.mode.threshold,
shortcut: args.mode.shortcut,
delay: args.mode.delay,
} : undefined,
});
}
/**
* Capture a shortcut using the client
* The callback takes (key, stop) where `stop` is a function that will stop capturing.
* This `stop` function must be called before disconnecting or else the user will have
* to restart their client.
* @param {Function} callback Callback handling keys
* @returns {Promise<Function>}
*/
captureShortcut(callback) {
const subid = subKey(RPCEvents.CAPTURE_SHORTCUT_CHANGE);
const stop = () => {
this._subscriptions.delete(subid);
return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'STOP' });
};
this._subscriptions.set(subid, ({ shortcut }) => {
callback(shortcut, stop);
});
return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'START' })
.then(() => stop);
}
/**
* Sets the presence for the logged in user.
* @param {object} args The rich presence to pass.
* @param {number} [pid] The application's process ID. Defaults to the executing process' PID.
* @returns {Promise}
*/
setActivity(args = {}, pid = getPid()) {
let timestamps;
let assets;
let party;
let secrets;
if (args.startTimestamp || args.endTimestamp) {
timestamps = {
start: args.startTimestamp,
end: args.endTimestamp,
};
if (timestamps.start instanceof Date) {
timestamps.start = Math.round(timestamps.start.getTime());
}
if (timestamps.end instanceof Date) {
timestamps.end = Math.round(timestamps.end.getTime());
}
if (timestamps.start > 2147483647000) {
throw new RangeError('timestamps.start must fit into a unix timestamp');
}
if (timestamps.end > 2147483647000) {
throw new RangeError('timestamps.end must fit into a unix timestamp');
}
}
if (
args.largeImageKey || args.largeImageText
|| args.smallImageKey || args.smallImageText
) {
assets = {
large_image: args.largeImageKey,
large_text: args.largeImageText,
small_image: args.smallImageKey,
small_text: args.smallImageText,
};
}
if (args.partySize || args.partyId || args.partyMax) {
party = { id: args.partyId };
if (args.partySize || args.partyMax) {
party.size = [args.partySize, args.partyMax];
}
}
if (args.matchSecret || args.joinSecret || args.spectateSecret) {
secrets = {
match: args.matchSecret,
join: args.joinSecret,
spectate: args.spectateSecret,
};
}
return this.request(RPCCommands.SET_ACTIVITY, {
pid,
activity: {
state: args.state,
details: args.details,
timestamps,
assets,
party,
secrets,
buttons: args.buttons,
instance: !!args.instance,
},
});
}
/**
* Clears the currently set presence, if any. This will hide the "Playing X" message
* displayed below the user's name.
* @param {number} [pid] The application's process ID. Defaults to the executing process' PID.
* @returns {Promise}
*/
clearActivity(pid = getPid()) {
return this.request(RPCCommands.SET_ACTIVITY, {
pid,
});
}
/**
* Invite a user to join the game the RPC user is currently playing
* @param {User} user The user to invite
* @returns {Promise}
*/
sendJoinInvite(user) {
return this.request(RPCCommands.SEND_ACTIVITY_JOIN_INVITE, {
user_id: user.id || user,
});
}
/**
* Request to join the game the user is playing
* @param {User} user The user whose game you want to request to join
* @returns {Promise}
*/
sendJoinRequest(user) {
return this.request(RPCCommands.SEND_ACTIVITY_JOIN_REQUEST, {
user_id: user.id || user,
});
}
/**
* Reject a join request from a user
* @param {User} user The user whose request you wish to reject
* @returns {Promise}
*/
closeJoinRequest(user) {
return this.request(RPCCommands.CLOSE_ACTIVITY_JOIN_REQUEST, {
user_id: user.id || user,
});
}
createLobby(type, capacity, metadata) {
return this.request(RPCCommands.CREATE_LOBBY, {
type,
capacity,
metadata,
});
}
updateLobby(lobby, { type, owner, capacity, metadata } = {}) {
return this.request(RPCCommands.UPDATE_LOBBY, {
id: lobby.id || lobby,
type,
owner_id: (owner && owner.id) || owner,
capacity,
metadata,
});
}
deleteLobby(lobby) {
return this.request(RPCCommands.DELETE_LOBBY, {
id: lobby.id || lobby,
});
}
connectToLobby(id, secret) {
return this.request(RPCCommands.CONNECT_TO_LOBBY, {
id,
secret,
});
}
sendToLobby(lobby, data) {
return this.request(RPCCommands.SEND_TO_LOBBY, {
id: lobby.id || lobby,
data,
});
}
disconnectFromLobby(lobby) {
return this.request(RPCCommands.DISCONNECT_FROM_LOBBY, {
id: lobby.id || lobby,
});
}
updateLobbyMember(lobby, user, metadata) {
return this.request(RPCCommands.UPDATE_LOBBY_MEMBER, {
lobby_id: lobby.id || lobby,
user_id: user.id || user,
metadata,
});
}
getRelationships() {
const types = Object.keys(RelationshipTypes);
return this.request(RPCCommands.GET_RELATIONSHIPS)
.then((o) => o.relationships.map((r) => ({
...r,
type: types[r.type],
})));
}
/**
* Subscribe to an event
* @param {string} event Name of event e.g. `MESSAGE_CREATE`
* @param {Object} [args] Args for event e.g. `{ channel_id: '1234' }`
* @returns {Promise<Object>}
*/
async subscribe(event, args) {
await this.request(RPCCommands.SUBSCRIBE, args, event);
return {
unsubscribe: () => this.request(RPCCommands.UNSUBSCRIBE, args, event),
};
}
/**
* Destroy the client
*/
async destroy() {
await this.transport.close();
}
}
module.exports = RPCClient;

View File

@@ -0,0 +1,178 @@
'use strict';
function keyMirror(arr) {
const tmp = {};
for (const value of arr) {
tmp[value] = value;
}
return tmp;
}
exports.browser = typeof window !== 'undefined';
exports.RPCCommands = keyMirror([
'DISPATCH',
'AUTHORIZE',
'AUTHENTICATE',
'GET_GUILD',
'GET_GUILDS',
'GET_CHANNEL',
'GET_CHANNELS',
'CREATE_CHANNEL_INVITE',
'GET_RELATIONSHIPS',
'GET_USER',
'SUBSCRIBE',
'UNSUBSCRIBE',
'SET_USER_VOICE_SETTINGS',
'SET_USER_VOICE_SETTINGS_2',
'SELECT_VOICE_CHANNEL',
'GET_SELECTED_VOICE_CHANNEL',
'SELECT_TEXT_CHANNEL',
'GET_VOICE_SETTINGS',
'SET_VOICE_SETTINGS_2',
'SET_VOICE_SETTINGS',
'CAPTURE_SHORTCUT',
'SET_ACTIVITY',
'SEND_ACTIVITY_JOIN_INVITE',
'CLOSE_ACTIVITY_JOIN_REQUEST',
'ACTIVITY_INVITE_USER',
'ACCEPT_ACTIVITY_INVITE',
'INVITE_BROWSER',
'DEEP_LINK',
'CONNECTIONS_CALLBACK',
'BRAINTREE_POPUP_BRIDGE_CALLBACK',
'GIFT_CODE_BROWSER',
'GUILD_TEMPLATE_BROWSER',
'OVERLAY',
'BROWSER_HANDOFF',
'SET_CERTIFIED_DEVICES',
'GET_IMAGE',
'CREATE_LOBBY',
'UPDATE_LOBBY',
'DELETE_LOBBY',
'UPDATE_LOBBY_MEMBER',
'CONNECT_TO_LOBBY',
'DISCONNECT_FROM_LOBBY',
'SEND_TO_LOBBY',
'SEARCH_LOBBIES',
'CONNECT_TO_LOBBY_VOICE',
'DISCONNECT_FROM_LOBBY_VOICE',
'SET_OVERLAY_LOCKED',
'OPEN_OVERLAY_ACTIVITY_INVITE',
'OPEN_OVERLAY_GUILD_INVITE',
'OPEN_OVERLAY_VOICE_SETTINGS',
'VALIDATE_APPLICATION',
'GET_ENTITLEMENT_TICKET',
'GET_APPLICATION_TICKET',
'START_PURCHASE',
'GET_SKUS',
'GET_ENTITLEMENTS',
'GET_NETWORKING_CONFIG',
'NETWORKING_SYSTEM_METRICS',
'NETWORKING_PEER_METRICS',
'NETWORKING_CREATE_TOKEN',
'SET_USER_ACHIEVEMENT',
'GET_USER_ACHIEVEMENTS',
]);
exports.RPCEvents = keyMirror([
'CURRENT_USER_UPDATE',
'GUILD_STATUS',
'GUILD_CREATE',
'CHANNEL_CREATE',
'RELATIONSHIP_UPDATE',
'VOICE_CHANNEL_SELECT',
'VOICE_STATE_CREATE',
'VOICE_STATE_DELETE',
'VOICE_STATE_UPDATE',
'VOICE_SETTINGS_UPDATE',
'VOICE_SETTINGS_UPDATE_2',
'VOICE_CONNECTION_STATUS',
'SPEAKING_START',
'SPEAKING_STOP',
'GAME_JOIN',
'GAME_SPECTATE',
'ACTIVITY_JOIN',
'ACTIVITY_JOIN_REQUEST',
'ACTIVITY_SPECTATE',
'ACTIVITY_INVITE',
'NOTIFICATION_CREATE',
'MESSAGE_CREATE',
'MESSAGE_UPDATE',
'MESSAGE_DELETE',
'LOBBY_DELETE',
'LOBBY_UPDATE',
'LOBBY_MEMBER_CONNECT',
'LOBBY_MEMBER_DISCONNECT',
'LOBBY_MEMBER_UPDATE',
'LOBBY_MESSAGE',
'CAPTURE_SHORTCUT_CHANGE',
'OVERLAY',
'OVERLAY_UPDATE',
'ENTITLEMENT_CREATE',
'ENTITLEMENT_DELETE',
'USER_ACHIEVEMENT_UPDATE',
'READY',
'ERROR',
]);
exports.RPCErrors = {
CAPTURE_SHORTCUT_ALREADY_LISTENING: 5004,
GET_GUILD_TIMED_OUT: 5002,
INVALID_ACTIVITY_JOIN_REQUEST: 4012,
INVALID_ACTIVITY_SECRET: 5005,
INVALID_CHANNEL: 4005,
INVALID_CLIENTID: 4007,
INVALID_COMMAND: 4002,
INVALID_ENTITLEMENT: 4015,
INVALID_EVENT: 4004,
INVALID_GIFT_CODE: 4016,
INVALID_GUILD: 4003,
INVALID_INVITE: 4011,
INVALID_LOBBY: 4013,
INVALID_LOBBY_SECRET: 4014,
INVALID_ORIGIN: 4008,
INVALID_PAYLOAD: 4000,
INVALID_PERMISSIONS: 4006,
INVALID_TOKEN: 4009,
INVALID_USER: 4010,
LOBBY_FULL: 5007,
NO_ELIGIBLE_ACTIVITY: 5006,
OAUTH2_ERROR: 5000,
PURCHASE_CANCELED: 5008,
PURCHASE_ERROR: 5009,
RATE_LIMITED: 5011,
SELECT_CHANNEL_TIMED_OUT: 5001,
SELECT_VOICE_FORCE_REQUIRED: 5003,
SERVICE_UNAVAILABLE: 1001,
TRANSACTION_ABORTED: 1002,
UNAUTHORIZED_FOR_ACHIEVEMENT: 5010,
UNKNOWN_ERROR: 1000,
};
exports.RPCCloseCodes = {
CLOSE_NORMAL: 1000,
CLOSE_UNSUPPORTED: 1003,
CLOSE_ABNORMAL: 1006,
INVALID_CLIENTID: 4000,
INVALID_ORIGIN: 4001,
RATELIMITED: 4002,
TOKEN_REVOKED: 4003,
INVALID_VERSION: 4004,
INVALID_ENCODING: 4005,
};
exports.LobbyTypes = {
PRIVATE: 1,
PUBLIC: 2,
};
exports.RelationshipTypes = {
NONE: 0,
FRIEND: 1,
BLOCKED: 2,
PENDING_INCOMING: 3,
PENDING_OUTGOING: 4,
IMPLICIT: 5,
};

View File

@@ -0,0 +1,10 @@
'use strict';
const util = require('./util');
module.exports = {
Client: require('./client'),
register(id) {
return util.register(`discord-${id}`);
},
};

View File

@@ -0,0 +1,6 @@
'use strict';
module.exports = {
ipc: require('./ipc'),
websocket: require('./websocket'),
};

View File

@@ -0,0 +1,173 @@
'use strict';
const net = require('net');
const EventEmitter = require('events');
const fetch = require('node-fetch');
const { uuid } = require('../util');
const OPCodes = {
HANDSHAKE: 0,
FRAME: 1,
CLOSE: 2,
PING: 3,
PONG: 4,
};
function getIPCPath(id) {
if (process.platform === 'win32') {
return `\\\\?\\pipe\\discord-ipc-${id}`;
}
const { env: { XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP } } = process;
const prefix = XDG_RUNTIME_DIR || TMPDIR || TMP || TEMP || '/tmp';
return `${prefix.replace(/\/$/, '')}/discord-ipc-${id}`;
}
function getIPC(id = 0) {
return new Promise((resolve, reject) => {
const path = getIPCPath(id);
const onerror = () => {
if (id < 10) {
resolve(getIPC(id + 1));
} else {
reject(new Error('Could not connect'));
}
};
const sock = net.createConnection(path, () => {
sock.removeListener('error', onerror);
resolve(sock);
});
sock.once('error', onerror);
});
}
async function findEndpoint(tries = 0) {
if (tries > 30) {
throw new Error('Could not find endpoint');
}
const endpoint = `http://127.0.0.1:${6463 + (tries % 10)}`;
try {
const r = await fetch(endpoint);
if (r.status === 404) {
return endpoint;
}
return findEndpoint(tries + 1);
} catch (e) {
return findEndpoint(tries + 1);
}
}
function encode(op, data) {
data = JSON.stringify(data);
const len = Buffer.byteLength(data);
const packet = Buffer.alloc(8 + len);
packet.writeInt32LE(op, 0);
packet.writeInt32LE(len, 4);
packet.write(data, 8, len);
return packet;
}
const working = {
full: '',
op: undefined,
};
function decode(socket, callback) {
const packet = socket.read();
if (!packet) {
return;
}
let { op } = working;
let raw;
if (working.full === '') {
op = working.op = packet.readInt32LE(0);
const len = packet.readInt32LE(4);
raw = packet.slice(8, len + 8);
} else {
raw = packet.toString();
}
try {
const data = JSON.parse(working.full + raw);
callback({ op, data }); // eslint-disable-line callback-return
working.full = '';
working.op = undefined;
} catch (err) {
working.full += raw;
}
decode(socket, callback);
}
class IPCTransport extends EventEmitter {
constructor(client) {
super();
this.client = client;
this.socket = null;
}
async connect() {
const socket = this.socket = await getIPC();
socket.on('close', this.onClose.bind(this));
socket.on('error', this.onClose.bind(this));
this.emit('open');
socket.write(encode(OPCodes.HANDSHAKE, {
v: 1,
client_id: this.client.clientId,
}));
socket.pause();
socket.on('readable', () => {
decode(socket, ({ op, data }) => {
switch (op) {
case OPCodes.PING:
this.send(data, OPCodes.PONG);
break;
case OPCodes.FRAME:
if (!data) {
return;
}
if (data.cmd === 'AUTHORIZE' && data.evt !== 'ERROR') {
findEndpoint()
.then((endpoint) => {
this.client.request.endpoint = endpoint;
})
.catch((e) => {
this.client.emit('error', e);
});
}
this.emit('message', data);
break;
case OPCodes.CLOSE:
this.emit('close', data);
break;
default:
break;
}
});
});
}
onClose(e) {
this.emit('close', e);
}
send(data, op = OPCodes.FRAME) {
this.socket.write(encode(op, data));
}
async close() {
return new Promise((r) => {
this.once('close', r);
this.send({}, OPCodes.CLOSE);
this.socket.end();
});
}
ping() {
this.send(uuid(), OPCodes.PING);
}
}
module.exports = IPCTransport;
module.exports.encode = encode;
module.exports.decode = decode;

View File

@@ -0,0 +1,77 @@
'use strict';
const EventEmitter = require('events');
const { browser } = require('../constants');
// eslint-disable-next-line
const WebSocket = browser ? window.WebSocket : require('ws');
const pack = (d) => JSON.stringify(d);
const unpack = (s) => JSON.parse(s);
class WebSocketTransport extends EventEmitter {
constructor(client) {
super();
this.client = client;
this.ws = null;
this.tries = 0;
}
async connect() {
const port = 6463 + (this.tries % 10);
this.tries += 1;
this.ws = new WebSocket(
`ws://127.0.0.1:${port}/?v=1&client_id=${this.client.clientId}`,
browser ? undefined : { origin: this.client.options.origin },
);
this.ws.onopen = this.onOpen.bind(this);
this.ws.onclose = this.onClose.bind(this);
this.ws.onerror = this.onError.bind(this);
this.ws.onmessage = this.onMessage.bind(this);
}
onOpen() {
this.emit('open');
}
onClose(event) {
if (!event.wasClean) {
return;
}
this.emit('close', event);
}
onError(event) {
try {
this.ws.close();
} catch {} // eslint-disable-line no-empty
if (this.tries > 20) {
this.emit('error', event.error);
} else {
setTimeout(() => {
this.connect();
}, 250);
}
}
onMessage(event) {
this.emit('message', unpack(event.data));
}
send(data) {
this.ws.send(pack(data));
}
ping() {} // eslint-disable-line no-empty-function
close() {
return new Promise((r) => {
this.once('close', r);
this.ws.close();
});
}
}
module.exports = WebSocketTransport;

View File

@@ -0,0 +1,50 @@
'use strict';
let register;
try {
const { app } = require('electron');
register = app.setAsDefaultProtocolClient.bind(app);
} catch (err) {
try {
register = require('register-scheme');
} catch (e) {} // eslint-disable-line no-empty
}
if (typeof register !== 'function') {
register = () => false;
}
function pid() {
if (typeof process !== 'undefined') {
return process.pid;
}
return null;
}
const uuid4122 = () => {
let uuid = '';
for (let i = 0; i < 32; i += 1) {
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
let n;
if (i === 12) {
n = 4;
} else {
const random = Math.random() * 16 | 0;
if (i === 16) {
n = (random & 3) | 0;
} else {
n = random;
}
}
uuid += n.toString(16);
}
return uuid;
};
module.exports = {
pid,
register,
uuid: uuid4122,
};

View File

@@ -1,4 +1,4 @@
const { app, BrowserWindow, BrowserView, globalShortcut, ipcMain, Tray, Menu, protocol, session } = require("electron"); const { app, BrowserWindow, BrowserView, globalShortcut, ipcMain, Tray, Menu, protocol, session, dialog } = require("electron");
const electronremote = require("@electron/remote/main"); const electronremote = require("@electron/remote/main");
//const asar = require('@electron/asar'); //const asar = require('@electron/asar');
const windowStateKeeper = require("electron-window-state"); const windowStateKeeper = require("electron-window-state");
@@ -7,7 +7,8 @@ const openAboutWindow = require("./about-window/src/index").default;
const badge = require('./badge'); const badge = require('./badge');
const contextMenu = require('./context-menu'); const contextMenu = require('./context-menu');
const autoUpdater = require('./utils/auto-update'); const autoUpdater = require('./utils/auto-update');
//const loadCRX = require('./utils/loadCRX'); const loadCRX = require('./utils/loadCRX');
const userStyles = require('./utils/userStyles');
const log4js = require("log4js"); const log4js = require("log4js");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
@@ -15,6 +16,14 @@ const os = require("os");
require('v8-compile-cache'); require('v8-compile-cache');
const packageJson = require(path.join(__dirname, '..', '..', 'package.json')); const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
const contributors = require(path.join(__dirname, 'contributors.json'));
// Check if app is ran from the installer dmg (macOS)
if (process.platform === 'darwin' && app.isPackaged && app.isInApplicationsFolder()) {
// Creeate a dialog to ask the user to move the app to the Applications folder
dialog.showErrorBox('Move to Applications folder', 'Please move the app to the Applications folder to ensure it works correctly.');
app.quit();
};
// isUpdaing: // isUpdaing:
global.isUpdating = false; global.isUpdating = false;
@@ -36,6 +45,8 @@ global.paths = {
temp: path.join(os.tmpdir(), global.appInfo.name), temp: path.join(os.tmpdir(), global.appInfo.name),
}; };
global.paths.updateDir = path.join(global.paths.data, 'update'); global.paths.updateDir = path.join(global.paths.data, 'update');
global.paths.extensions = path.join(global.paths.data, 'extensions');
global.paths.userstyles = path.join(global.paths.data, 'userstyles');
// URLs: // URLs:
global.urls = { global.urls = {
@@ -99,7 +110,7 @@ if (fs.existsSync(logFile) && !fs.existsSync(path.join(global.paths.data, 'lockf
fs.renameSync(logFile, path.join(global.paths.data, `${logFileName}.${mtime.toISOString().split('T')[0]}.log`)); fs.renameSync(logFile, path.join(global.paths.data, `${logFileName}.${mtime.toISOString().split('T')[0]}.log`));
}; };
}; };
logger.log("Starting Bsky Desktop"); logger.log(`Starting Bsky Desktop v${packageJson.version} on ${os.platform()} ${os.arch()}`);
// Create data directory if it does not exist: // Create data directory if it does not exist:
if (!fs.existsSync(global.paths.data)) { if (!fs.existsSync(global.paths.data)) {
@@ -119,6 +130,18 @@ if (!fs.existsSync(global.paths.updateDir)) {
fs.mkdirSync(global.paths.updateDir, { recursive: true }); fs.mkdirSync(global.paths.updateDir, { recursive: true });
}; };
// Create extensions directory if it does not exist:
if (!fs.existsSync(global.paths.extensions)) {
logger.info("Creating Extensions Directory");
fs.mkdirSync(global.paths.extensions, { recursive: true });
};
// Create userstyles directory if it does not exist:
if (!fs.existsSync(global.paths.userstyles)) {
logger.info("Creating Userstyles Directory");
fs.mkdirSync(global.paths.userstyles, { recursive: true });
};
// improve performance on linux? // improve performance on linux?
if (process.platform !== "win32" && process.platform !== "darwin") { if (process.platform !== "win32" && process.platform !== "darwin") {
logger.log("Disabling Hardware Acceleration and Transparent Visuals"); logger.log("Disabling Hardware Acceleration and Transparent Visuals");
@@ -267,7 +290,7 @@ function createWindow() {
}; };
}); });
PageView.webContents.setWindowOpenHandler(({ url }) => { PageView.webContents.setWindowOpenHandler(({ url }) => {
new BrowserWindow({ show: true, autoHideMenuBar: true }).loadURL(url); new BrowserWindow({ show: true, autoHideMenuBar: true, icon: path.join(global.paths.app, 'ui', 'images', 'logo.png') }).loadURL(url);
return { action: 'deny' }; return { action: 'deny' };
}); });
// Log PageView navigations: // Log PageView navigations:
@@ -287,6 +310,7 @@ function showAboutWindow() {
//open_devtools: process.env.NODE_ENV !== 'production', //open_devtools: process.env.NODE_ENV !== 'production',
use_version_info: [ use_version_info: [
['Application Version', `${global.appInfo.version}`], ['Application Version', `${global.appInfo.version}`],
['Contributors', contributors.map((contributor) => contributor.name).join(', ')],
], ],
license: `MIT, GPL-2.0, GPL-3.0, ${global.appInfo.license}`, license: `MIT, GPL-2.0, GPL-3.0, ${global.appInfo.license}`,
}); });
@@ -294,7 +318,7 @@ function showAboutWindow() {
function createTray() { function createTray() {
logger.log("Creating Tray"); logger.log("Creating Tray");
const tray = new Tray(path.join(global.paths.app, 'ui', 'images', 'logo.png')); const tray = new Tray(path.join(global.paths.app, 'ui', 'images', 'icons', '32x32.png'));
tray.setToolTip('Bsky Desktop'); tray.setToolTip('Bsky Desktop');
tray.setContextMenu(Menu.buildFromTemplate([ tray.setContextMenu(Menu.buildFromTemplate([
{ label: global.appInfo.name, enabled: false }, { label: global.appInfo.name, enabled: false },
@@ -370,7 +394,7 @@ function handleDeeplink(commandLine) {
break; break;
case "notiftest": case "notiftest":
global.PageView.webContents.send('ui:notif', { title: 'Updater', message: 'Update downloaded', options: { position: 'topRight', timeout: 5000, layout: 2, color: 'blue' } }); global.PageView.webContents.send('ui:notif', { title: 'Updater', message: 'Update downloaded', options: { izitoast: { position: 'topRight', timeout: 5000, layout: 2, color: 'blue' } } });
break; break;
default: default:
@@ -492,6 +516,70 @@ app.whenReady().then(() => {
createWindow(); createWindow();
createTray(); createTray();
// Load extensions (.crx files):
logger.log("Checking for extensions");
const extensions = fs.readdirSync(global.paths.extensions).filter((file) => file.endsWith('.crx'));
if (extensions.length > 0) {
logger.log(`Unpacking ${extensions.length} extensions and loading them`);
extensions.forEach((extension) => {
loadCRX(path.join(global.paths.extensions, extension));
});
} else {
// Check for unpacked extensions:
const unpackedExtensions = fs.readdirSync(global.paths.extensions).filter((file) => fs.lstatSync(path.join(global.paths.extensions, file)).isDirectory());
// Check if the directory contains a manifest.json file
unpackedExtensions.forEach((extension) => {
const manifestPath = path.join(global.paths.extensions, extension, 'manifest.json');
if (fs.existsSync(manifestPath)) {
logger.log(`Loading unpacked extension: ${extension}`);
session.defaultSession.loadExtension(path.join(global.paths.extensions, extension)).then(({ id }) => {
logger.log(`Extension loaded with ID: ${id}`);
}).catch((error) => {
logger.error(`Failed to load extension: ${error}`);
});
} else {
logger.warn(`Skipping directory ${extension} as it does not contain a manifest.json file`);
};
});
};
// Load userstyles
logger.log("Checking for userstyles");
const userStylesDir = path.join(global.paths.userstyles);
if (fs.existsSync(userStylesDir)) {
const files = fs.readdirSync(userStylesDir);
if (files.length > 0) {
const userStylePromises = files.map(async file => {
const cssFile = path.join(userStylesDir, file);
// Parse the CSS file
try {
const cssContent = fs.readFileSync(cssFile, 'utf-8');
const result = await userStyles.parseCSS(cssContent);
logger.info(`Loading userstyle: ${result.metadata.name}`);
// Compile the userstyle
const compiled = await userStyles.compileStyle(result.css, result.metadata);
// Check if the site 'bsky.app' is defined
if (compiled.sites?.['bsky.app']) {
// Apply the userstyle to the PageView
await PageView.webContents.insertCSS(compiled.sites['bsky.app']);
logger.info(`Applied userstyle: ${result.metadata.name}`);
} else {
logger.warn(`Userstyle ${result.metadata.name} does not target 'bsky.app'`);
}
} catch (error) {
logger.error(`Error loading userstyle: ${file}`, error);
}
});
Promise.all(userStylePromises);
}
}
} else { } else {
logger.log("Failed to get singleInstanceLock, Quitting"); logger.log("Failed to get singleInstanceLock, Quitting");
app.quit(); app.quit();

View File

@@ -80,7 +80,7 @@ function asarUpdate() {
function checkForUpdates() { function checkForUpdates() {
// Current system information // Current system information
logger.log('Current system information:', sys.platform, sys.getVersion()); logger.log('Current system information:', sys.platform, sys.getVersion(), sys.arch);
// Check if the current system is Windows // Check if the current system is Windows
if (sys.isWin()) { if (sys.isWin()) {
@@ -107,13 +107,14 @@ function checkForUpdates() {
// System is ARM64 (mac-arm64) // System is ARM64 (mac-arm64)
macArch = 'arm64'; macArch = 'arm64';
} else { } else {
// System is Intel (mac-x64) // System is x64 (mac-x64)
macArch = 'intel'; macArch = 'x64';
} }
} else { } else {
// macOS 10 is mostly Intel, but some versions are ARM64 // macOS 10 is mostly x64, but some versions are ARM64
macArch = 'intel'; macArch = 'x64';
} }
logger.log('System architecture:', macArch);
// Check for updates, and if there are updates, download and install them // Check for updates, and if there are updates, download and install them
logger.log('Checking for updates (mac)...'); logger.log('Checking for updates (mac)...');
@@ -122,7 +123,7 @@ function checkForUpdates() {
const command = `sudo installer -pkg ${pkgPath} -target /`; const command = `sudo installer -pkg ${pkgPath} -target /`;
// Spawn a new shell // Spawn a new shell
const shellProcess = spawn('sh', ['-c', command], { /*const shellProcess = spawn('sh', ['-c', command], {
stdio: 'inherit', // Pipe input/output to/from the shell stdio: 'inherit', // Pipe input/output to/from the shell
}); });
@@ -136,7 +137,7 @@ function checkForUpdates() {
} else { } else {
console.error(`Shell process exited with code ${code}.`); console.error(`Shell process exited with code ${code}.`);
} }
}); });*/
} else { } else {
// macOS versions before 10 are not supported // macOS versions before 10 are not supported
logger.error('macOS versions before 10 are not supported, not updating...'); logger.error('macOS versions before 10 are not supported, not updating...');

View File

@@ -3,30 +3,91 @@ const path = require('path');
const { session } = require('electron'); const { session } = require('electron');
const AdmZip = require('adm-zip'); const AdmZip = require('adm-zip');
/**
* Converts a CRX file buffer to a ZIP buffer.
* @param {Buffer} buf - The CRX file buffer.
* @returns {Buffer} - The ZIP buffer extracted from the CRX file.
*/
function crxToZip(buf) {
function calcLength(a, b, c, d) {
let length = 0;
length += a << 0;
length += b << 8;
length += c << 16;
length += (d << 24) >>> 0;
return length;
}
// Check if the file is already a ZIP file
if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {
return buf;
}
// Validate CRX magic number
if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {
throw new Error('Invalid CRX file: Missing Cr24 magic number');
}
const version = buf[4];
const isV2 = version === 2;
const isV3 = version === 3;
if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {
throw new Error('Unsupported CRX format version.');
}
if (isV2) {
const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);
const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);
const zipStartOffset = 16 + publicKeyLength + signatureLength;
return buf.slice(zipStartOffset);
}
const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);
const zipStartOffset = 12 + headerSize;
return buf.slice(zipStartOffset);
}
/** /**
* Unpacks a .crx file and loads it as an Electron extension. * Unpacks a .crx file and loads it as an Electron extension.
* @param {string} crxPath - Path to the .crx file. * @param {string} crxPath - Path to the .crx file.
* @returns {Promise<string>} - Resolves with the extension ID after loading. * @returns {Promise<string>} - Resolves with the extension ID after loading.
*/ */
async function loadCRX(crxPath) { async function loadCRX(crxPath) {
const outputDir = path.join(__dirname, 'extensions', path.basename(crxPath, '.crx')); const outputDir = path.join(global.paths.extensions, path.basename(crxPath, '.crx'));
// Ensure the output directory exists // Ensure the output directory exists
if (!fs.existsSync(outputDir)) { if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true }); fs.mkdirSync(outputDir, { recursive: true });
// Extract the .crx file // Read the CRX file
const crxData = fs.readFileSync(crxPath); const crxData = fs.readFileSync(crxPath);
const crxHeaderSize = crxData.readUInt32LE(8); // Extract header size from CRX
const zipData = crxData.slice(crxHeaderSize);
// Save the ZIP content // Convert CRX to ZIP
const zipData = crxToZip(crxData);
// Extract ZIP using AdmZip
const zip = new AdmZip(zipData); const zip = new AdmZip(zipData);
zip.extractAllTo(outputDir, true); zip.getEntries().forEach((entry) => {
const fullPath = path.join(outputDir, entry.entryName);
if (entry.isDirectory) {
fs.mkdirSync(fullPath, { recursive: true });
} else {
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, entry.getData());
}
});
} }
// Load the unpacked extension into Electron // Load the unpacked extension into Electron
try { try {
// Check for manifest.json
const manifestPath = path.join(outputDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
throw new Error('Extension is missing manifest.json');
};
// Load the extension
const { id } = await session.defaultSession.loadExtension(outputDir); const { id } = await session.defaultSession.loadExtension(outputDir);
console.log(`Extension loaded with ID: ${id}`); console.log(`Extension loaded with ID: ${id}`);
return id; return id;

210
src/app/utils/userStyles.js Normal file
View File

@@ -0,0 +1,210 @@
const usercssMeta = require('usercss-meta');
const less = require('less');
/**
* Extracts all global variable and mixin definitions from CSS.
* @param {string} css - The CSS string.
* @returns {string} The extracted global definitions.
*/
function extractGlobalDefinitions(css) {
const globalDefinitionRegex = /(@[\w-]+\s*(?:{[^}]*}|;))/g;
return (css.match(globalDefinitionRegex) || []).join('\n');
}
/**
* Extracts variable definitions from metadata.
* @param {object} metadata - Metadata containing variable definitions.
* @returns {object} A map of variable names to their default values.
*/
function extractMetadataVars(metadata) {
if (!metadata?.vars) return {};
return Object.fromEntries(
Object.entries(metadata.vars).map(([key, value]) => [key, value.default || value.value || null])
);
}
/**
* Extracts content enclosed within matching braces starting from a given position.
* @param {string} css - The CSS string.
* @param {number} startPos - The starting position to search for braces.
* @returns {object|null} The content and ending position of the matched braces.
*/
function extractBracedContent(css, startPos) {
const braceMatch = matchBraces(css, startPos);
if (!braceMatch) return null;
return {
content: css.substring(braceMatch.start + 1, braceMatch.end - 1).trim(),
end: braceMatch.end,
};
}
/**
* Matches a pair of braces in a string starting from a given position.
* @param {string} content - The string content.
* @param {number} start - The starting position to search for braces.
* @returns {object|null} The start and end positions of the matched braces.
*/
function matchBraces(content, start) {
const openBrace = content.indexOf('{', start);
if (openBrace === -1) return null;
let braceCount = 1, pos = openBrace + 1;
while (braceCount > 0 && pos < content.length) {
if (content[pos] === '{') braceCount++;
if (content[pos] === '}') braceCount--;
pos++;
}
return braceCount === 0 ? { start: openBrace, end: pos } : null;
}
/**
* Parses domain rules from the provided CSS string.
* @param {string} css - The CSS string.
* @param {number} startPos - The starting position to search for domain rules.
* @returns {object|null} The domains and the rule start position.
*/
function parseDomainRule(css, startPos) {
const domainRuleRegex = /@-moz-document\s+domain\(\s*'([^']+)'(?:\s*,\s*'([^']+)')*\s*\)/g;
domainRuleRegex.lastIndex = startPos;
const match = domainRuleRegex.exec(css);
if (!match) return null;
const domains = match[0]
.match(/'[^']+'/g) // Extract all single-quoted domain values
.map(domain => domain.replace(/'/g, '').trim());
return {
domains,
ruleStart: match.index + match[0].length - 1,
};
}
/**
* Parses @-moz-document rules and extracts domain-specific CSS.
* @param {string} css - The CSS string.
* @returns {object} A map of domains to their associated CSS.
*/
function parseMozRules(css) {
const rules = {};
let currentPos = 0;
const globalCode = extractGlobalDefinitions(css);
while (true) {
const domainRule = parseDomainRule(css, currentPos);
if (!domainRule) break;
const bracedContent = extractBracedContent(css, domainRule.ruleStart);
if (!bracedContent) break;
// Add the CSS to all matched domains
for (const domain of domainRule.domains) {
rules[domain] = `${globalCode}\n${bracedContent.content}`;
}
currentPos = bracedContent.end;
}
return rules;
}
/**
* Parses metadata from the provided CSS string.
* @param {string} css - The CSS string.
* @returns {object} Parsed metadata.
*/
function parseCSS(css) {
try {
const normalizedCss = css.replace(/\r\n?/g, '\n');
const nocommentsCss = normalizedCss.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove comments
return {
...usercssMeta.parse(normalizedCss),
css: nocommentsCss,
};
} catch (error) {
throw new Error(`Failed to parse CSS metadata: ${error.message}`);
}
}
/**
* Compiles CSS code with a preprocessor if specified in the metadata.
* @param {string} code - The CSS code.
* @param {object} metadata - Metadata containing preprocessor information.
* @param {object} [userVars={}] - User-defined variables to override defaults.
* @returns {Promise<{compiledCss: string, sites: object}>} The compiled CSS code and domain-specific mapping.
*/
async function compileStyle(code, metadata, userVars = {}) {
try {
// Extract and merge variables
const vars = {
...extractMetadataVars(metadata),
...userVars
};
// Generate full code with user variables
const fullCode = [
'// User variables',
Object.entries(vars).map(([key, value]) => `@${key}: ${value};`).join('\n'),
'// Main code',
code
].join('\n\n');
let compiledCode;
switch (metadata?.preprocessor?.toLowerCase()) {
case 'less':
compiledCode = await compileLess(fullCode);
break;
default:
compiledCode = code; // Return unmodified for unknown preprocessors
}
// Parse domain rules
const domainRules = parseMozRules(compiledCode);
// Compile each domain's CSS if needed
const compiledRules = {};
for (const [domain, css] of Object.entries(domainRules)) {
if (metadata.preprocessor === 'less') {
compiledRules[domain] = await compileLess(css, vars);
} else {
compiledRules[domain] = css;
}
}
// Combine all CSS
const combinedCss = Object.entries(compiledRules)
.map(([domain, compiledCss]) => `/* ${domain} */\n${compiledCss}`)
.join('\n\n');
return {
compiledCss: combinedCss,
sites: compiledRules // Map of domains to their CSS
};
} catch (error) {
console.error('Style compilation error:', error);
throw error;
}
}
/**
* Compiles LESS code to CSS.
* @param {string} code - The LESS code.
* @returns {Promise<string>} The compiled CSS.
*/
async function compileLess(code) {
const { css } = await less.render(code, {
math: 'parens-division',
javascriptEnabled: true,
compress: false
});
return css;
}
module.exports = {
parseCSS,
compileStyle
};

View File

@@ -4,4 +4,4 @@
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform> <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform>
<animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1"></animate> <animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1"></animate>
</circle> </circle>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 812 B

BIN
src/ui/images/mac.icns Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
<svg class="spinner" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="60" height="60" style="box-sizing: border-box; padding: 3px; overflow: visible; color: #0071c2;">
<circle cx="60" cy="60" r="50" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" style="transform-origin: center;">
<animateTransform attributeName="transform" type="rotate" values="-90;810" keyTimes="0;1" dur="2s" repeatCount="indefinite"></animateTransform>
<animate attributeName="stroke-dashoffset" values="0%;0%;-157.080%" calcMode="spline" keySplines="0.61, 1, 0.88, 1; 0.12, 0, 0.39, 0" keyTimes="0;0.5;1" dur="2s" repeatCount="indefinite"></animate>
<animate attributeName="stroke-dasharray" values="0% 314.159%;157.080% 157.080%;0% 314.159%" calcMode="spline" keySplines="0.61, 1, 0.88, 1; 0.12, 0, 0.39, 0" keyTimes="0;0.5;1" dur="2s" repeatCount="indefinite"></animate>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 920 B

File diff suppressed because one or more lines are too long

11
src/ui/lib/css/stylelint-bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

63
src/ui/lib/css/stylus-renderer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -47,10 +47,14 @@ document.addEventListener('DOMContentLoaded', () => {
// Load jQuery first // Load jQuery first
loadScript('ui:///lib/jquery-3.3.1.min.js', () => { loadScript('ui:///lib/jquery-3.3.1.min.js', () => {
// Load iziToast after jQuery // Load confetti and iziToast after jQuery (ui libs)
loadScript('ui:///lib/confetti-1.9.3-browser.min.js');
loadScript('ui:///lib/izitoast.min.js', () => { loadScript('ui:///lib/izitoast.min.js', () => {
// Load app specific scripts
loadScript('ui:///rend/register-handles.js'); loadScript('ui:///rend/register-handles.js');
// Load custom implementations
loadScript('ui:///rend/bsky-ext.js'); loadScript('ui:///rend/bsky-ext.js');
loadScript('ui:///rend/specialAnimations.js');
}); });
}); });
}); });

View File

@@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', () => {
message: event.message, message: event.message,
position: 'topRight', position: 'topRight',
timeout: 5000, timeout: 5000,
...event.options, ...event.options.izitoast,
}); });
} catch (error) { } catch (error) {
console.error('Failed to display notification:', error); console.error('Failed to display notification:', error);

View File

@@ -0,0 +1,109 @@
const anims = {
imgs: {
// tree shape from https://thenounproject.com/icon/pine-tree-1471679/
tree: confetti.shapeFromPath({
path: 'M120 240c-41,14 -91,18 -120,1 29,-10 57,-22 81,-40 -18,2 -37,3 -55,-3 25,-14 48,-30 66,-51 -11,5 -26,8 -45,7 20,-14 40,-30 57,-49 -13,1 -26,2 -38,-1 18,-11 35,-25 51,-43 -13,3 -24,5 -35,6 21,-19 40,-41 53,-67 14,26 32,48 54,67 -11,-1 -23,-3 -35,-6 15,18 32,32 51,43 -13,3 -26,2 -38,1 17,19 36,35 56,49 -19,1 -33,-2 -45,-7 19,21 42,37 67,51 -19,6 -37,5 -56,3 25,18 53,30 82,40 -30,17 -79,13 -120,-1l0 41 -31 0 0 -41z',
matrix: [0.03597122302158273, 0, 0, 0.03597122302158273, -4.856115107913669, -5.071942446043165]
}),
// pumpkin shape from https://thenounproject.com/icon/pumpkin-5253388/
pumpkin: confetti.shapeFromPath({
path: 'M449.4 142c-5 0-10 .3-15 1a183 183 0 0 0-66.9-19.1V87.5a17.5 17.5 0 1 0-35 0v36.4a183 183 0 0 0-67 19c-4.9-.6-9.9-1-14.8-1C170.3 142 105 219.6 105 315s65.3 173 145.7 173c5 0 10-.3 14.8-1a184.7 184.7 0 0 0 169 0c4.9.7 9.9 1 14.9 1 80.3 0 145.6-77.6 145.6-173s-65.3-173-145.7-173zm-220 138 27.4-40.4a11.6 11.6 0 0 1 16.4-2.7l54.7 40.3a11.3 11.3 0 0 1-7 20.3H239a11.3 11.3 0 0 1-9.6-17.5zM444 383.8l-43.7 17.5a17.7 17.7 0 0 1-13 0l-37.3-15-37.2 15a17.8 17.8 0 0 1-13 0L256 383.8a17.5 17.5 0 0 1 13-32.6l37.3 15 37.2-15c4.2-1.6 8.8-1.6 13 0l37.3 15 37.2-15a17.5 17.5 0 0 1 13 32.6zm17-86.3h-82a11.3 11.3 0 0 1-6.9-20.4l54.7-40.3a11.6 11.6 0 0 1 16.4 2.8l27.4 40.4a11.3 11.3 0 0 1-9.6 17.5z',
matrix: [0.020491803278688523, 0, 0, 0.020491803278688523, -7.172131147540983, -5.9016393442622945]
}),
// heart shape from https://thenounproject.com/icon/heart-1545381/
heart: confetti.shapeFromPath({
path: 'M167 72c19,-38 37,-56 75,-56 42,0 76,33 76,75 0,76 -76,151 -151,227 -76,-76 -151,-151 -151,-227 0,-42 33,-75 75,-75 38,0 57,18 76,56z',
matrix: [0.03333333333333333, 0, 0, 0.03333333333333333, -5.566666666666666, -5.533333333333333]
})
},
// Function to animate the snow effect
snow: async function () {
var duration = 15 * 1000;
var animationEnd = Date.now() + duration;
var skew = 1;
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
// Function to animate the first snow particle
async function snowAnimation() {
return new Promise(resolve => {
var firstSnowDuration = 3000; // 3 seconds
var firstAnimationEnd = Date.now() + firstSnowDuration;
(function firstSnowFrame() {
var timeLeft = firstAnimationEnd - Date.now();
confetti({
particleCount: 1,
startVelocity: 0,
ticks: Math.max(200, 500 * (timeLeft / firstSnowDuration)),
origin: {
x: Math.random(),
y: randomInRange(0.2, 0.4) * skew - 0.4
},
colors: ['#ffffff'],
shapes: ['circle'],
gravity: randomInRange(0.4, 0.6),
scalar: randomInRange(0.4, 1),
drift: randomInRange(-0.4, 0.4)
});
if (timeLeft > 0) {
requestAnimationFrame(firstSnowFrame);
} else {
resolve(); // Resolve the promise when the animation is complete
}
})();
});
}
// Function to animate the continuous snow particles
function mainSnowAnimation() {
(function frame() {
var timeLeft = animationEnd - Date.now();
var ticks = Math.max(200, 500 * (timeLeft / duration));
skew = Math.max(0.8, skew - 0.001);
confetti({
particleCount: 1,
startVelocity: 0,
ticks: ticks,
origin: {
x: Math.random(),
y: randomInRange(0.2, 0.4) * skew - 0.4
},
colors: ['#ffffff'],
shapes: ['circle'],
gravity: randomInRange(0.4, 0.6),
scalar: randomInRange(0.4, 1),
drift: randomInRange(-0.4, 0.4)
});
confetti({
particleCount: 1,
startVelocity: 0,
ticks: ticks,
origin: {
x: Math.random(),
y: randomInRange(0.2, 0.4) * skew - 0.2
},
colors: ['#ffffff'],
shapes: ['circle'],
gravity: randomInRange(0.4, 0.6),
scalar: randomInRange(0.4, 1),
drift: randomInRange(-0.4, 0.4)
});
if (timeLeft > 0) {
requestAnimationFrame(frame);
}
})();
}
// Start the animations
await snowAnimation(); // Wait for the first snow animation to complete
mainSnowAnimation(); // Start the continuous snow animation
}
};