Compare commits
158 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e3f165857 | ||
|
|
8a9d621456 | ||
|
|
3055e283a1 | ||
|
|
44ee4970f9 | ||
|
|
90797d6dd9 | ||
|
|
194199daed | ||
|
|
19a1b41e6d | ||
|
|
c5e8c49bd9 | ||
|
|
398451d7c7 | ||
|
|
2ca7e2e0d2 | ||
|
|
95bb71b60f | ||
|
|
692c4834cd | ||
|
|
3ca0b7b395 | ||
|
|
a47440a3ef | ||
|
|
c8f8212d7a | ||
|
|
657bf6d0d0 | ||
|
|
9d361c35cc | ||
|
|
93d2558324 | ||
|
|
4f5cbbb3c2 | ||
|
|
cc8ba75694 | ||
|
|
225c623ecb | ||
|
|
1eb09a589a | ||
|
|
821ff30d40 | ||
|
|
d98d6d5441 | ||
|
|
ac7f85f679 | ||
|
|
222d796843 | ||
|
|
0abe72a3c6 | ||
|
|
4b05dd4fbe | ||
|
|
0e40c6c6ec | ||
|
|
4f13d893cd | ||
|
|
593271c2b9 | ||
|
|
dec110d795 | ||
|
|
8b0e6588fd | ||
|
|
74c941e5b8 | ||
|
|
00c7572cd0 | ||
|
|
0c2c081ab0 | ||
|
|
35ee57483a | ||
|
|
a632ecde33 | ||
|
|
4f845f58a9 | ||
|
|
d165a77875 | ||
|
|
7ca75e859e | ||
|
|
c330333ee6 | ||
|
|
c15d64164b | ||
|
|
4208ee0ec2 | ||
|
|
50b05be8fc | ||
|
|
a81c7b092b | ||
|
|
5d1a5e3ecb | ||
|
|
b3c4959c7c | ||
|
|
841a9be2cf | ||
|
|
60fb61a1db | ||
|
|
24c9cc51f4 | ||
|
|
da383611c0 | ||
|
|
741102d6eb | ||
|
|
22785a9860 | ||
|
|
e56455fbaf | ||
|
|
0c2d23d029 | ||
|
|
363f9f675b | ||
|
|
c386cb6cba | ||
|
|
b5f8edeb4d | ||
|
|
b9a3d37c9a | ||
|
|
962e4b188b | ||
|
|
6fbd8a18f6 | ||
|
|
b22130645a | ||
|
|
7d07e3fcc8 | ||
|
|
b59ab85e7a | ||
|
|
5a3c1458ac | ||
|
|
262c74fefe | ||
|
|
da075829dc | ||
|
|
60dbbde2af | ||
|
|
c0eb4129ac | ||
|
|
2c7a864815 | ||
|
|
590729ccff | ||
|
|
c407a89142 | ||
|
|
fa6cfb86f3 | ||
|
|
b292308cd5 | ||
|
|
a60a60ab4b | ||
|
|
2f2b4474b4 | ||
|
|
0e1c696a68 | ||
|
|
f31c2ca067 | ||
|
|
fbf53123fa | ||
|
|
85aaf5946d | ||
|
|
cec88cfe3f | ||
|
|
0e903e71df | ||
|
|
f95cc126ce | ||
|
|
09662fc37e | ||
|
|
efbba2a65f | ||
|
|
149930460b | ||
|
|
05decfbae4 | ||
|
|
727ba0f1df | ||
|
|
a7115dc1d4 | ||
|
|
5e0b70056e | ||
|
|
a63ce3c41f | ||
|
|
0dd42efcaa | ||
|
|
ac1b93677c | ||
|
|
73af6e3062 | ||
|
|
47e524f0d2 | ||
|
|
191e23b61f | ||
|
|
94610845be | ||
|
|
34fd294be9 | ||
|
|
7110c2202f | ||
|
|
e5731054a7 | ||
|
|
1e8310874c | ||
|
|
95715a4fcd | ||
|
|
a61fbb695a | ||
|
|
b2bb58b9f0 | ||
|
|
97662d2e15 | ||
|
|
a9f67aa099 | ||
|
|
0b849eb62b | ||
|
|
ff169347df | ||
|
|
b6cd8524dc | ||
|
|
a8af809134 | ||
|
|
ef57607bc6 | ||
|
|
63d735f265 | ||
|
|
5a0959d721 | ||
|
|
82c81a6424 | ||
|
|
4bcbd24d99 | ||
|
|
671e67cde4 | ||
|
|
0e8441e709 | ||
|
|
8d32b73d98 | ||
|
|
02003514df | ||
|
|
a1463ea803 | ||
|
|
457f2e5b21 | ||
|
|
23241d8777 | ||
|
|
fe3a7c479b | ||
|
|
02185d005a | ||
|
|
d1bf862a6c | ||
|
|
9a5ecbb22c | ||
|
|
542023c6a5 | ||
|
|
cbec0b1804 | ||
|
|
982088c96d | ||
|
|
77c2337b84 | ||
|
|
901e416b89 | ||
|
|
9db9cd9373 | ||
|
|
02f1eb08ff | ||
|
|
910b356071 | ||
|
|
63d5e94e6c | ||
|
|
a5327b96cc | ||
|
|
bba0bb2c0e | ||
|
|
a926345013 | ||
|
|
5ac04dff7e | ||
|
|
dad8a675e2 | ||
|
|
db0e1b9647 | ||
|
|
581cf4fce5 | ||
|
|
3d738bfe0e | ||
|
|
dd90d70560 | ||
|
|
8b81078429 | ||
|
|
76bbebec1b | ||
|
|
d0196cd675 | ||
|
|
5ad6819747 | ||
|
|
449c819214 | ||
|
|
e9b04f7dc5 | ||
|
|
92ec5e1a9f | ||
|
|
c9165c682e | ||
|
|
f9c3108590 | ||
|
|
f9a01a3a1a | ||
|
|
356964d1ee | ||
|
|
bb93c62e92 | ||
|
|
994381411d |
56 changed files with 7600 additions and 1300 deletions
88
.github/workflows/build.yml
vendored
88
.github/workflows/build.yml
vendored
|
|
@ -1,13 +1,20 @@
|
||||||
|
name: Build and Release Sanctum
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build App
|
name: Build App
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -16,13 +23,80 @@ jobs:
|
||||||
- name: Checkout assets
|
- name: Checkout assets
|
||||||
run: git -c submodule."assets".update=checkout submodule update --init assets
|
run: git -c submodule."assets".update=checkout submodule update --init assets
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Node
|
||||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
node-version: 20
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install pnpm
|
||||||
|
run: npm install -g pnpm@10.18.1
|
||||||
|
|
||||||
|
- name: Install System Dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y zip jq curl
|
||||||
|
|
||||||
|
- name: Install Project Dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build
|
- name: Build Linux & Windows
|
||||||
run: pnpm run package
|
# We run both; if one fails, the whole job stops before reaching release logic
|
||||||
|
run: |
|
||||||
|
pnpm exec electron-forge make --platform linux --arch x64 --targets @electron-forge/maker-zip
|
||||||
|
pnpm exec electron-forge make --platform win32 --arch x64 --targets @electron-forge/maker-zip
|
||||||
|
|
||||||
|
- name: Create or Update Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
BASE_URL: "https://git.mithraic.cloud/api/v1/repos/${{ github.repository }}"
|
||||||
|
run: |
|
||||||
|
echo "Processing release for $TAG..."
|
||||||
|
|
||||||
|
# 1. Try to get existing release ID
|
||||||
|
RELEASE_ID=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$BASE_URL/releases/tags/$TAG" | jq -r '.id // empty')
|
||||||
|
|
||||||
|
# 2. Create release if it doesn't exist
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Creating new release..."
|
||||||
|
RELEASE_ID=$(curl -s -X POST "$BASE_URL/releases" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"Sanctum $TAG\",\"draft\":false,\"prerelease\":false}" | jq -r '.id')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "::error::Could not determine Release ID"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Release ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
# 3. Upload loop with conflict handling
|
||||||
|
find out/make -type f -name "*.zip" | while read FILE; do
|
||||||
|
NAME=$(basename "$FILE")
|
||||||
|
echo "Targeting asset: $NAME"
|
||||||
|
|
||||||
|
# Check for existing asset with same name
|
||||||
|
EXISTING_ASSET_ID=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$BASE_URL/releases/$RELEASE_ID/assets" | \
|
||||||
|
jq -r ".[] | select(.name==\"$NAME\") | .id // empty")
|
||||||
|
|
||||||
|
if [ ! -z "$EXISTING_ASSET_ID" ]; then
|
||||||
|
echo "Asset already exists (ID: $EXISTING_ASSET_ID). Deleting to avoid conflict..."
|
||||||
|
curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" "$BASE_URL/releases/$RELEASE_ID/assets/$EXISTING_ASSET_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Uploading $NAME..."
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
"$BASE_URL/releases/$RELEASE_ID/assets?name=$NAME" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@$FILE")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" -eq 201 ] || [ "$HTTP_CODE" -eq 200 ]; then
|
||||||
|
echo "✅ Successfully uploaded $NAME"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to upload $NAME (HTTP $HTTP_CODE)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
88
.github/workflows/release-please.yml
vendored
88
.github/workflows/release-please.yml
vendored
|
|
@ -1,88 +0,0 @@
|
||||||
name: Release Please
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main] # updates/opens the release PR when commits land on main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: release-please
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-please:
|
|
||||||
name: Release Please
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
release_created: ${{ steps.rp.outputs.release_created }}
|
|
||||||
steps:
|
|
||||||
- id: app-token
|
|
||||||
uses: actions/create-github-app-token@v2
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.GH_STOAT_RELEASE_APP_ID }}
|
|
||||||
private-key: ${{ secrets.GH_STOAT_RELEASE_APP_PRIVATE_KEY }}
|
|
||||||
- id: rp
|
|
||||||
uses: googleapis/release-please-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
config-file: release-please-config.json
|
|
||||||
|
|
||||||
publish-release:
|
|
||||||
name: Publish App
|
|
||||||
needs: release-please
|
|
||||||
if: needs.release-please.outputs.release_created == 'true'
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Checkout assets
|
|
||||||
run: git -c submodule."assets".update=checkout submodule update --init assets
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
run: |
|
|
||||||
pnpm run publish
|
|
||||||
env:
|
|
||||||
PLATFORM: ${{ matrix.os }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish macOS x64
|
|
||||||
if: matrix.os == 'macos-latest'
|
|
||||||
run: pnpm run publish --arch=x64
|
|
||||||
env:
|
|
||||||
PLATFORM: ${{ matrix.os }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish Linux arm64
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
run: pnpm run publish --arch=arm64
|
|
||||||
env:
|
|
||||||
PLATFORM: ${{ matrix.os }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
7
.github/workflows/release-webhook.yml
vendored
7
.github/workflows/release-webhook.yml
vendored
|
|
@ -1,3 +1,6 @@
|
||||||
|
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||||
|
# Managed in Terraform templates
|
||||||
|
|
||||||
name: Release Webhook
|
name: Release Webhook
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -8,7 +11,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
release-webhook:
|
release-webhook:
|
||||||
name: Send Release Webhook
|
name: Send Release Webhook
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Send release notification webhook
|
- name: Send release notification webhook
|
||||||
|
|
@ -20,4 +23,4 @@ jobs:
|
||||||
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
|
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||||
curl -X POST "$WEBHOOK_URL" \
|
curl -X POST "$WEBHOOK_URL" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"content\": \"$RELEASE_URL\"}"
|
-d "{\"content\": \"$RELEASE_URL\"}"
|
||||||
|
|
|
||||||
7
.github/workflows/validate-pr-title.yml
vendored
7
.github/workflows/validate-pr-title.yml
vendored
|
|
@ -1,3 +1,6 @@
|
||||||
|
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||||
|
# Managed in Terraform templates
|
||||||
|
|
||||||
name: "Lint PR"
|
name: "Lint PR"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -11,10 +14,10 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v6
|
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
12
GEMINI.md
Normal file
12
GEMINI.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Agent Mandates
|
||||||
|
|
||||||
|
## Versioning and Release Workflow
|
||||||
|
Before every `git push` that includes code changes, you MUST perform the following steps:
|
||||||
|
|
||||||
|
1. **Bump Version:** Increment the version in `package.json` (both `version` and `aviaVersion`).
|
||||||
|
2. **Update Branding:** If a version string is hardcoded in UI plugins, update it to match the new version.
|
||||||
|
3. **Migration Logic:** Update any migration logic in plugins to ensure users on the previous version are automatically updated to the new default.
|
||||||
|
4. **Tagging:** Create the git tag corresponding to the new version with a 'v' prefix (e.g., `git tag v1.0.x`).
|
||||||
|
5. **Push:** Push both the branch and the tags to the remote repository (`git push origin main --tags`).
|
||||||
|
|
||||||
|
This ensures the internal app state matches the release tag and prevents auto-updater loops.
|
||||||
11
README.md
11
README.md
|
|
@ -3,6 +3,7 @@
|
||||||
Avia Client for Desktop
|
Avia Client for Desktop
|
||||||
"stoat desktop"
|
"stoat desktop"
|
||||||
</h1>
|
</h1>
|
||||||
|
<img width="256" height="256" alt="aurora" src="https://github.com/user-attachments/assets/dc3adfa3-ce3b-41ef-bdfd-9ca66d333e24" /><br />
|
||||||
Application for Windows, macOS, and Linux. now with avia client injected
|
Application for Windows, macOS, and Linux. now with avia client injected
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
@ -13,7 +14,8 @@ Application for Windows, macOS, and Linux. now with avia client injected
|
||||||
<img src="https://repology.org/badge/vertical-allrepos/stoat-desktop.svg" alt="Packaging status" align="right">
|
<img src="https://repology.org/badge/vertical-allrepos/stoat-desktop.svg" alt="Packaging status" align="right">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
- All downloads and instructions for Stoat can be found on our [Website](https://stoat.chat/download).
|
- If you use the Browser you can find FireFox/Chrome/Userscript Builds at [BrowserBuilds](https://github.com/AvaLilac/Ava-Client).
|
||||||
|
- Though I reccomend you use Userscript if on Chrome Based Browsers. As Plugins do not exist due to browser limits in Extensions. Userscript fine though
|
||||||
|
|
||||||
## Development Guide
|
## Development Guide
|
||||||
|
|
||||||
|
|
@ -32,6 +34,11 @@ Then proceed to setup:
|
||||||
```bash
|
```bash
|
||||||
# clone the repository
|
# clone the repository
|
||||||
git clone --recursive https://github.com/AvaLilac/for-desktop aviaclient-for-desktop
|
git clone --recursive https://github.com/AvaLilac/for-desktop aviaclient-for-desktop
|
||||||
|
|
||||||
|
# clone the repository (If you are building from developer branch. Which is not always stable)
|
||||||
|
git clone -b dev --recursive https://github.com/AvaLilac/for-desktop aviaclient-for-desktop
|
||||||
|
|
||||||
|
# CD into the directory
|
||||||
cd aviaclient-for-desktop
|
cd aviaclient-for-desktop
|
||||||
|
|
||||||
# install all packages
|
# install all packages
|
||||||
|
|
@ -64,3 +71,5 @@ pnpm run:nix --force-server=http://localhost:5173
|
||||||
# a better solution would be telling
|
# a better solution would be telling
|
||||||
# Electron Forge where system Electron is
|
# Electron Forge where system Electron is
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`VCSounds.js` ships as a built-in local plugin now, so it is seeded automatically on launch and cannot be accidentally removed from the release install.
|
||||||
|
|
|
||||||
611
about.html
Normal file
611
about.html
Normal file
|
|
@ -0,0 +1,611 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>About Aviaclient</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500&family=Google+Sans+Display:wght@400;500&family=Roboto:wght@400;500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--md-primary: #adc6ff;
|
||||||
|
--md-primary-container: #003e9c;
|
||||||
|
--md-on-primary-container: #d8e2ff;
|
||||||
|
--md-surface: #131318;
|
||||||
|
--md-surface-container: #1e1e24;
|
||||||
|
--md-surface-container-high: #28282e;
|
||||||
|
--md-outline-variant: #46464f;
|
||||||
|
--md-on-surface: #e4e1e9;
|
||||||
|
--md-on-surface-variant: #c7c5d0;
|
||||||
|
--md-secondary: #bec6dc;
|
||||||
|
--md-secondary-container: #3f4759;
|
||||||
|
--md-on-secondary-container: #dbe2f9;
|
||||||
|
--md-tertiary-container: #5c3349;
|
||||||
|
--md-tertiary: #efb8c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--md-surface);
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
padding: 14px 24px 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.topbar-title {
|
||||||
|
font-family: "Google Sans", sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: clamp(160px, 26%, 240px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--md-surface-container);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 20px;
|
||||||
|
gap: 14px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
animation: fadeIn 0.25s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.sidebar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-text {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.sidebar-text h1 {
|
||||||
|
font-family: "Google Sans Display", "Google Sans", sans-serif;
|
||||||
|
font-size: clamp(15px, 2.2vw, 21px);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.sidebar-text p {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: clamp(11px, 1.3vw, 13px);
|
||||||
|
color: var(--md-on-surface-variant);
|
||||||
|
line-height: 1.55;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stoatVersion {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15px;
|
||||||
|
margin-left: 45px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar #logo {
|
||||||
|
position: absolute;
|
||||||
|
width: 200px;
|
||||||
|
left: 25px;
|
||||||
|
top: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 3px 12px;
|
||||||
|
background: var(--md-secondary-container);
|
||||||
|
color: var(--md-on-secondary-container);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-family: "Google Sans", sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 16px 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.main::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.main::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.main::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-family: "Google Sans", sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--md-primary);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 4px 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.section-label:first-child {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--md-surface-container);
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:first-child {
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
.list-item:last-child {
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
}
|
||||||
|
.list-item:only-child {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-icon-row {
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
.license-body {
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 11px 14px;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.list-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.list-item:active {
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item + .list-item::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 58px;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--md-outline-variant);
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--md-primary-container);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.item-icon svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
fill: var(--md-primary);
|
||||||
|
}
|
||||||
|
.item-icon.secondary {
|
||||||
|
background: var(--md-secondary-container);
|
||||||
|
}
|
||||||
|
.item-icon.secondary svg {
|
||||||
|
fill: var(--md-secondary);
|
||||||
|
}
|
||||||
|
.item-icon.tertiary {
|
||||||
|
background: var(--md-tertiary-container);
|
||||||
|
}
|
||||||
|
.item-icon.tertiary svg {
|
||||||
|
fill: var(--md-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.item-title {
|
||||||
|
font-family: "Google Sans", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--md-on-surface);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.item-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--md-on-surface-variant);
|
||||||
|
margin-top: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-trail {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.item-trail svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: var(--md-on-surface-variant);
|
||||||
|
opacity: 0.45;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.item-badge {
|
||||||
|
font-family: "Google Sans", sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--md-on-surface-variant);
|
||||||
|
background: var(--md-surface-container-high);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-icon-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.license-body {
|
||||||
|
padding: 10px 14px 14px;
|
||||||
|
}
|
||||||
|
.license-body p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--md-on-surface-variant);
|
||||||
|
line-height: 1.65;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.license-body a {
|
||||||
|
color: var(--md-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.license-body a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 460px) {
|
||||||
|
.layout {
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 14px 16px;
|
||||||
|
gap: 14px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.sidebar-text {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.sidebar-text p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
animation: fadeIn 0.25s ease both;
|
||||||
|
}
|
||||||
|
.main > * {
|
||||||
|
animation: slideUp 0.25s ease both;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(1) {
|
||||||
|
animation-delay: 0.06s;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(2) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(3) {
|
||||||
|
animation-delay: 0.13s;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(4) {
|
||||||
|
animation-delay: 0.16s;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(5) {
|
||||||
|
animation-delay: 0.19s;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(6) {
|
||||||
|
animation-delay: 0.22s;
|
||||||
|
}
|
||||||
|
.main > *:nth-child(7) {
|
||||||
|
animation-delay: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="topbar">
|
||||||
|
<span class="topbar-title">About</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<div class="sidebar">
|
||||||
|
<img id="logo" title="Aurora" src="avia_assets/icon.png" />
|
||||||
|
<div class="sidebar-text">
|
||||||
|
<h1>Aviaclient</h1>
|
||||||
|
<div id="aviaVersion" class="version-chip">TBD</div>
|
||||||
|
<p>
|
||||||
|
A custom desktop client with enhancements and additional features.
|
||||||
|
</p>
|
||||||
|
<div id="stoatVersion">Based on Stoat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="section-label">Links</div>
|
||||||
|
<div class="card">
|
||||||
|
<a
|
||||||
|
class="list-item"
|
||||||
|
href="https://github.com/AvaLilac/for-desktop"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div class="item-icon">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">Source code</div>
|
||||||
|
<div class="item-sub">github.com/AvaLilac/for-desktop</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-trail">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="list-item"
|
||||||
|
href="https://github.com/AvaLilac/for-desktop/issues"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div class="item-icon secondary">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">Issues</div>
|
||||||
|
<div class="item-sub">Report bugs or request features</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-trail">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="list-item"
|
||||||
|
href="https://github.com/AvaLilac/for-desktop/tree/dev"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div class="item-icon tertiary">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">Dev branch</div>
|
||||||
|
<div class="item-sub">Latest development changes</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-trail">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-label">License</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="license-icon-row">
|
||||||
|
<div class="item-icon tertiary">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">GNU AGPL v3.0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="license-body">
|
||||||
|
<p>
|
||||||
|
Licensed under the
|
||||||
|
<a
|
||||||
|
href="https://www.gnu.org/licenses/agpl-3.0.txt"
|
||||||
|
target="_blank"
|
||||||
|
>GNU Affero General Public License v3.0</a
|
||||||
|
>. You are free to use, modify, and distribute this software under
|
||||||
|
the same license.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-label">Open source</div>
|
||||||
|
<div class="card">
|
||||||
|
<a
|
||||||
|
class="list-item"
|
||||||
|
href="https://github.com/electron/electron"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div class="item-icon">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">Electron</div>
|
||||||
|
<div class="item-sub">Desktop runtime framework</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-trail">
|
||||||
|
<span class="item-badge">runtime</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="list-item"
|
||||||
|
href="https://github.com/electron-userland/electron-builder"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div class="item-icon secondary">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">Electron Forge</div>
|
||||||
|
<div class="item-sub">Packaging and distribution</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-trail">
|
||||||
|
<span class="item-badge">packaging</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="list-item"
|
||||||
|
href="https://github.com/discordjs/RPC"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div class="item-icon tertiary">
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="item-text">
|
||||||
|
<div class="item-title">discord-rpc</div>
|
||||||
|
<div class="item-sub">Discord Rich Presence integration</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-trail"><span class="item-badge">rpc</span></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
const aviaElem = document.getElementById("aviaVersion");
|
||||||
|
if (aviaElem) {
|
||||||
|
aviaElem.textContent = window.native.versions.aviaClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
const stoatElem = document.getElementById("stoatVersion");
|
||||||
|
if (stoatElem) {
|
||||||
|
stoatElem.textContent = `Based on Stoat ${window.native.versions.desktop()}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
avia_assets/icon.ico
Normal file
BIN
avia_assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 325 KiB |
BIN
avia_assets/icon.icon/Assets/IMG_0248.PNG
Normal file
BIN
avia_assets/icon.icon/Assets/IMG_0248.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 MiB |
75
avia_assets/icon.icon/icon.json
Normal file
75
avia_assets/icon.icon/icon.json
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"fill-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : {
|
||||||
|
"solid" : "display-p3:0.33912,0.12908,0.93430,1.00000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : {
|
||||||
|
"automatic-gradient" : "display-p3:0.15333,0.05837,0.42244,1.00000",
|
||||||
|
"orientation" : {
|
||||||
|
"start" : {
|
||||||
|
"x" : 0.5,
|
||||||
|
"y" : 0
|
||||||
|
},
|
||||||
|
"stop" : {
|
||||||
|
"x" : 0.5,
|
||||||
|
"y" : 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"blend-mode-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : "normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "tinted",
|
||||||
|
"value" : "normal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fill-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : "automatic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "tinted",
|
||||||
|
"value" : "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"glass" : false,
|
||||||
|
"image-name" : "IMG_0248.PNG",
|
||||||
|
"name" : "IMG_0248",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.25,
|
||||||
|
"translation-in-points" : [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
avia_assets/icon.png
Normal file
BIN
avia_assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
avia_assets/iconTemplate.png
Normal file
BIN
avia_assets/iconTemplate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
29
avia_core/ButtonFix.js
Normal file
29
avia_core/ButtonFix.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__BUTTON_FIX__) return;
|
||||||
|
window.__BUTTON_FIX__ = true;
|
||||||
|
|
||||||
|
function uninjectButton(button){
|
||||||
|
if(button){
|
||||||
|
button.parentElement.removeChild(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(()=>{
|
||||||
|
let balls = [];
|
||||||
|
document.querySelectorAll('div[class=\'flex-sh_0 d_flex ai_end jc_center w_42px\']').forEach(element=>{
|
||||||
|
if(element.id?.includes('avia')){
|
||||||
|
balls.push(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const gifSpan = [...document.querySelectorAll("span.material-symbols-outlined")]
|
||||||
|
.find(s => s.textContent.trim() === "gif");
|
||||||
|
|
||||||
|
if(!gifSpan){
|
||||||
|
balls.forEach(element=>{
|
||||||
|
uninjectButton(element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {childList: true, subtree: true })
|
||||||
|
})();
|
||||||
688
avia_core/LocalPlugins.js
Normal file
688
avia_core/LocalPlugins.js
Normal file
|
|
@ -0,0 +1,688 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
if (window.__AVIA_LOCAL_PLUGINS_LOADED__) return;
|
||||||
|
window.__AVIA_LOCAL_PLUGINS_LOADED__ = true;
|
||||||
|
|
||||||
|
const STORAGE_KEY = "avia_local_plugins";
|
||||||
|
const BUILTIN_SEED = Array.isArray(window.__SANCTUM_BUILTIN_LOCAL_PLUGINS__)
|
||||||
|
? window.__SANCTUM_BUILTIN_LOCAL_PLUGINS__
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const runningLocalPlugins = {};
|
||||||
|
const localPluginErrors = {};
|
||||||
|
|
||||||
|
const getLocalPlugins = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
|
const setLocalPlugins = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
function upsertBuiltinLocalPlugins() {
|
||||||
|
if (!BUILTIN_SEED.length) return;
|
||||||
|
|
||||||
|
const plugins = getLocalPlugins();
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
for (const builtin of BUILTIN_SEED) {
|
||||||
|
const next = {
|
||||||
|
id: builtin.id,
|
||||||
|
name: builtin.name,
|
||||||
|
code: builtin.code || "",
|
||||||
|
enabled: true,
|
||||||
|
locked: true,
|
||||||
|
builtin: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingIndex = plugins.findIndex((plugin) =>
|
||||||
|
plugin.id === next.id || plugin.name === next.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
const current = plugins[existingIndex];
|
||||||
|
const merged = {
|
||||||
|
...current,
|
||||||
|
...next,
|
||||||
|
enabled: true,
|
||||||
|
locked: true,
|
||||||
|
builtin: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
JSON.stringify(current) !== JSON.stringify(merged)
|
||||||
|
) {
|
||||||
|
plugins[existingIndex] = merged;
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plugins.push(next);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty) setLocalPlugins(plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadMonaco() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (window.monaco) return resolve();
|
||||||
|
const loader = document.createElement("script");
|
||||||
|
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
|
||||||
|
loader.onload = function () {
|
||||||
|
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
|
||||||
|
require(["vs/editor/editor.main"], () => resolve());
|
||||||
|
};
|
||||||
|
document.head.appendChild(loader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runLocalPlugin(plugin) {
|
||||||
|
stopLocalPlugin(plugin);
|
||||||
|
try {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.textContent = plugin.code || "";
|
||||||
|
script.dataset.localPluginId = plugin.id;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
runningLocalPlugins[plugin.id] = script;
|
||||||
|
delete localPluginErrors[plugin.id];
|
||||||
|
} catch (e) {
|
||||||
|
localPluginErrors[plugin.id] = true;
|
||||||
|
}
|
||||||
|
renderLocalPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopLocalPlugin(plugin) {
|
||||||
|
const script = runningLocalPlugins[plugin.id];
|
||||||
|
if (!script) return;
|
||||||
|
script.remove();
|
||||||
|
delete runningLocalPlugins[plugin.id];
|
||||||
|
delete localPluginErrors[plugin.id];
|
||||||
|
renderLocalPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openEditorPanel(plugin, onSave) {
|
||||||
|
await preloadMonaco();
|
||||||
|
|
||||||
|
const existing = document.getElementById("avia-local-editor-panel");
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.id = "avia-local-editor-panel";
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "24px",
|
||||||
|
left: "24px",
|
||||||
|
width: "680px",
|
||||||
|
height: "460px",
|
||||||
|
background: "var(--md-sys-color-surface, #1e1e1e)",
|
||||||
|
borderRadius: "16px",
|
||||||
|
boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
|
||||||
|
zIndex: "9999999",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
backdropFilter: "blur(12px)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.textContent = `Editing: ${plugin.name}`;
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "14px 16px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "14px",
|
||||||
|
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move",
|
||||||
|
color: "#fff",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("div");
|
||||||
|
closeBtn.textContent = "✕";
|
||||||
|
Object.assign(closeBtn.style, {
|
||||||
|
position: "absolute",
|
||||||
|
top: "12px",
|
||||||
|
right: "16px",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: "0.7",
|
||||||
|
color: "#fff",
|
||||||
|
zIndex: "1"
|
||||||
|
});
|
||||||
|
closeBtn.onmouseenter = () => closeBtn.style.opacity = "1";
|
||||||
|
closeBtn.onmouseleave = () => closeBtn.style.opacity = "0.7";
|
||||||
|
closeBtn.onclick = () => panel.remove();
|
||||||
|
|
||||||
|
const toolbar = document.createElement("div");
|
||||||
|
Object.assign(toolbar.style, {
|
||||||
|
padding: "8px 16px",
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveBtn = document.createElement("button");
|
||||||
|
saveBtn.textContent = "💾 Save";
|
||||||
|
styleEditorBtn(saveBtn, "#2d6a4f");
|
||||||
|
|
||||||
|
const saveRunBtn = document.createElement("button");
|
||||||
|
saveRunBtn.textContent = "▶ Save & Run";
|
||||||
|
styleEditorBtn(saveRunBtn, "#1b4332");
|
||||||
|
|
||||||
|
toolbar.appendChild(saveBtn);
|
||||||
|
toolbar.appendChild(saveRunBtn);
|
||||||
|
|
||||||
|
const editorContainer = document.createElement("div");
|
||||||
|
editorContainer.style.flex = "1";
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(closeBtn);
|
||||||
|
panel.appendChild(toolbar);
|
||||||
|
panel.appendChild(editorContainer);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
const editor = monaco.editor.create(editorContainer, {
|
||||||
|
value: plugin.code || "// Write your plugin code here\n",
|
||||||
|
language: "javascript",
|
||||||
|
theme: "vs-dark",
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: "on"
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn.onclick = () => {
|
||||||
|
onSave(editor.getValue(), false);
|
||||||
|
saveBtn.textContent = "✓ Saved";
|
||||||
|
setTimeout(() => saveBtn.textContent = "💾 Save", 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
saveRunBtn.onclick = () => {
|
||||||
|
onSave(editor.getValue(), true);
|
||||||
|
saveRunBtn.textContent = "✓ Ran!";
|
||||||
|
setTimeout(() => saveRunBtn.textContent = "▶ Save & Run", 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
enableEditorDrag(panel, header);
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleEditorBtn(btn, bg) {
|
||||||
|
Object.assign(btn.style, {
|
||||||
|
padding: "5px 14px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: bg || "rgba(255,255,255,0.1)",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "500"
|
||||||
|
});
|
||||||
|
btn.onmouseenter = () => btn.style.opacity = "0.8";
|
||||||
|
btn.onmouseleave = () => btn.style.opacity = "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableEditorDrag(panel, handle) {
|
||||||
|
let isDragging = false, offsetX, offsetY;
|
||||||
|
handle.addEventListener("mousedown", e => {
|
||||||
|
isDragging = true;
|
||||||
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
isDragging = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
panel.style.left = (e.clientX - offsetX) + "px";
|
||||||
|
panel.style.top = (e.clientY - offsetY) + "px";
|
||||||
|
panel.style.right = "auto";
|
||||||
|
panel.style.bottom = "auto";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLocalPanel() {
|
||||||
|
let panel = document.getElementById("avia-local-plugins-panel");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = panel.style.display === "none" ? "flex" : "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "avia-local-plugins-panel";
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "24px",
|
||||||
|
right: "560px",
|
||||||
|
width: "520px",
|
||||||
|
height: "460px",
|
||||||
|
background: "var(--md-sys-color-surface, #1e1e1e)",
|
||||||
|
color: "var(--md-sys-color-on-surface, #fff)",
|
||||||
|
borderRadius: "16px",
|
||||||
|
boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
|
||||||
|
zIndex: "999999",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
backdropFilter: "blur(12px)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.textContent = "Local Plugins";
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "14px 16px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "14px",
|
||||||
|
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move"
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("div");
|
||||||
|
closeBtn.textContent = "✕";
|
||||||
|
Object.assign(closeBtn.style, {
|
||||||
|
position: "absolute",
|
||||||
|
top: "12px",
|
||||||
|
right: "16px",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: "0.7"
|
||||||
|
});
|
||||||
|
closeBtn.onclick = () => panel.style.display = "none";
|
||||||
|
|
||||||
|
const controlsBar = document.createElement("div");
|
||||||
|
Object.assign(controlsBar.style, {
|
||||||
|
padding: "12px 16px",
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
alignItems: "center",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameInput = document.createElement("input");
|
||||||
|
nameInput.placeholder = "Plugin name";
|
||||||
|
styleLocalInput(nameInput);
|
||||||
|
nameInput.style.flex = "1";
|
||||||
|
|
||||||
|
const addBtn = document.createElement("button");
|
||||||
|
addBtn.textContent = "+ New";
|
||||||
|
styleLocalBtn(addBtn);
|
||||||
|
addBtn.onclick = () => {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const plugins = getLocalPlugins();
|
||||||
|
const newPlugin = {
|
||||||
|
id: "local_" + Date.now(),
|
||||||
|
name,
|
||||||
|
code: "// " + name + "\n",
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
plugins.push(newPlugin);
|
||||||
|
setLocalPlugins(plugins);
|
||||||
|
nameInput.value = "";
|
||||||
|
renderLocalPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const importBtn = document.createElement("button");
|
||||||
|
importBtn.textContent = "Import";
|
||||||
|
styleLocalBtn(importBtn, "#2d6a4f");
|
||||||
|
importBtn.onmouseenter = () => importBtn.style.opacity = "0.75";
|
||||||
|
importBtn.onmouseleave = () => importBtn.style.opacity = "1";
|
||||||
|
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.accept = ".js";
|
||||||
|
fileInput.multiple = true;
|
||||||
|
fileInput.style.display = "none";
|
||||||
|
|
||||||
|
importBtn.onclick = () => fileInput.click();
|
||||||
|
|
||||||
|
fileInput.onchange = async () => {
|
||||||
|
const files = [...fileInput.files];
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
const plugins = getLocalPlugins();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const text = await file.text();
|
||||||
|
const name = file.name.replace(/\.js$/i, "");
|
||||||
|
plugins.push({
|
||||||
|
id: "local_" + Date.now() + "_" + Math.random(),
|
||||||
|
name,
|
||||||
|
code: text,
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalPlugins(plugins);
|
||||||
|
fileInput.value = "";
|
||||||
|
renderLocalPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
controlsBar.appendChild(nameInput);
|
||||||
|
controlsBar.appendChild(addBtn);
|
||||||
|
controlsBar.appendChild(importBtn);
|
||||||
|
controlsBar.appendChild(fileInput);
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.id = "avia-local-plugins-content";
|
||||||
|
Object.assign(content.style, {
|
||||||
|
flex: "1",
|
||||||
|
overflow: "auto",
|
||||||
|
padding: "16px"
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(closeBtn);
|
||||||
|
panel.appendChild(controlsBar);
|
||||||
|
panel.appendChild(content);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
const dropOverlay = document.createElement("div");
|
||||||
|
dropOverlay.textContent = "Import JS files";
|
||||||
|
Object.assign(dropOverlay.style, {
|
||||||
|
position: "absolute",
|
||||||
|
inset: "0",
|
||||||
|
background: "rgba(0,0,0,0.6)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#fff",
|
||||||
|
opacity: "0",
|
||||||
|
pointerEvents: "none",
|
||||||
|
transition: "opacity 0.15s ease",
|
||||||
|
borderRadius: "16px"
|
||||||
|
});
|
||||||
|
panel.appendChild(dropOverlay);
|
||||||
|
|
||||||
|
let dragDepth = 0;
|
||||||
|
|
||||||
|
panel.addEventListener("dragenter", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dragDepth++;
|
||||||
|
dropOverlay.style.opacity = "1";
|
||||||
|
panel.style.border = "1px dashed rgba(255,255,255,0.4)";
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("dragover", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("dragleave", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dragDepth--;
|
||||||
|
if (dragDepth <= 0) {
|
||||||
|
dropOverlay.style.opacity = "0";
|
||||||
|
panel.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||||
|
dragDepth = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("drop", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
dropOverlay.style.opacity = "0";
|
||||||
|
panel.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||||
|
dragDepth = 0;
|
||||||
|
|
||||||
|
const files = [...e.dataTransfer.files].filter(f => f.name.endsWith(".js"));
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
const plugins = getLocalPlugins();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const text = await file.text();
|
||||||
|
const name = file.name.replace(/\.js$/i, "");
|
||||||
|
plugins.push({
|
||||||
|
id: "local_" + Date.now() + "_" + Math.random(),
|
||||||
|
name,
|
||||||
|
code: text,
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalPlugins(plugins);
|
||||||
|
renderLocalPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
let isDragging = false, offsetX, offsetY;
|
||||||
|
header.addEventListener("mousedown", e => {
|
||||||
|
isDragging = true;
|
||||||
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => isDragging = false);
|
||||||
|
document.addEventListener("mousemove", e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
panel.style.left = (e.clientX - offsetX) + "px";
|
||||||
|
panel.style.top = (e.clientY - offsetY) + "px";
|
||||||
|
panel.style.right = "auto";
|
||||||
|
panel.style.bottom = "auto";
|
||||||
|
});
|
||||||
|
|
||||||
|
renderLocalPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLocalPanel() {
|
||||||
|
const content = document.getElementById("avia-local-plugins-content");
|
||||||
|
if (!content) return;
|
||||||
|
content.innerHTML = "";
|
||||||
|
const plugins = getLocalPlugins();
|
||||||
|
|
||||||
|
if (plugins.length === 0) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.textContent = "No local plugins yet. Add one above.";
|
||||||
|
empty.style.opacity = "0.4";
|
||||||
|
empty.style.fontSize = "13px";
|
||||||
|
content.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.forEach((plugin, index) => {
|
||||||
|
const isRunning = !!runningLocalPlugins[plugin.id];
|
||||||
|
const hasError = !!localPluginErrors[plugin.id];
|
||||||
|
const isBuiltin = !!plugin.locked || !!plugin.builtin;
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
Object.assign(row.style, {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "12px",
|
||||||
|
padding: "10px 12px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
background: "rgba(255,255,255,0.04)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.06)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const left = document.createElement("div");
|
||||||
|
Object.assign(left.style, { display: "flex", alignItems: "center", gap: "10px" });
|
||||||
|
|
||||||
|
const statusDot = document.createElement("div");
|
||||||
|
Object.assign(statusDot.style, { width: "10px", height: "10px", borderRadius: "50%", flexShrink: "0" });
|
||||||
|
if (hasError) {
|
||||||
|
statusDot.style.background = "#ff4d4d";
|
||||||
|
statusDot.style.boxShadow = "0 0 6px #ff4d4d";
|
||||||
|
} else if (isRunning) {
|
||||||
|
statusDot.style.background = "#4dff88";
|
||||||
|
statusDot.style.boxShadow = "0 0 6px #4dff88";
|
||||||
|
} else {
|
||||||
|
statusDot.style.background = "#777";
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = document.createElement("div");
|
||||||
|
name.textContent = plugin.name;
|
||||||
|
name.style.fontSize = "13px";
|
||||||
|
|
||||||
|
left.appendChild(statusDot);
|
||||||
|
left.appendChild(name);
|
||||||
|
|
||||||
|
if (isBuiltin) {
|
||||||
|
const badge = document.createElement("div");
|
||||||
|
badge.textContent = "Built-in";
|
||||||
|
Object.assign(badge.style, {
|
||||||
|
fontSize: "10px",
|
||||||
|
padding: "2px 7px",
|
||||||
|
borderRadius: "999px",
|
||||||
|
background: "rgba(120,170,255,0.16)",
|
||||||
|
color: "#a9c4ff",
|
||||||
|
border: "1px solid rgba(120,170,255,0.28)",
|
||||||
|
marginLeft: "2px",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.06em",
|
||||||
|
});
|
||||||
|
left.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls = document.createElement("div");
|
||||||
|
Object.assign(controls.style, { display: "flex", gap: "6px" });
|
||||||
|
|
||||||
|
if (!isBuiltin) {
|
||||||
|
const editBtn = document.createElement("button");
|
||||||
|
editBtn.textContent = "✏ Edit";
|
||||||
|
styleLocalBtn(editBtn, "rgba(100,140,255,0.2)");
|
||||||
|
editBtn.onclick = () => {
|
||||||
|
openEditorPanel(plugin, (newCode, andRun) => {
|
||||||
|
const all = getLocalPlugins();
|
||||||
|
const target = all.find(p => p.id === plugin.id);
|
||||||
|
if (target) {
|
||||||
|
target.code = newCode;
|
||||||
|
plugin.code = newCode;
|
||||||
|
setLocalPlugins(all);
|
||||||
|
}
|
||||||
|
if (andRun) {
|
||||||
|
plugin.enabled = true;
|
||||||
|
if (target) target.enabled = true;
|
||||||
|
setLocalPlugins(getLocalPlugins().map(p => p.id === plugin.id ? { ...p, code: newCode, enabled: true } : p));
|
||||||
|
runLocalPlugin(plugin);
|
||||||
|
}
|
||||||
|
renderLocalPanel();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBtn = document.createElement("button");
|
||||||
|
toggleBtn.textContent = plugin.enabled ? "Disable" : "Enable";
|
||||||
|
styleLocalBtn(toggleBtn);
|
||||||
|
toggleBtn.onclick = () => {
|
||||||
|
const all = getLocalPlugins();
|
||||||
|
const target = all.find(p => p.id === plugin.id);
|
||||||
|
if (!target) return;
|
||||||
|
target.enabled = !target.enabled;
|
||||||
|
plugin.enabled = target.enabled;
|
||||||
|
setLocalPlugins(all);
|
||||||
|
if (target.enabled) runLocalPlugin(plugin);
|
||||||
|
else stopLocalPlugin(plugin);
|
||||||
|
renderLocalPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("button");
|
||||||
|
removeBtn.textContent = "✕";
|
||||||
|
styleLocalBtn(removeBtn, "rgba(255,80,80,0.15)");
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
stopLocalPlugin(plugin);
|
||||||
|
const editorPanel = document.getElementById("avia-local-editor-panel");
|
||||||
|
if (editorPanel) editorPanel.remove();
|
||||||
|
const all = getLocalPlugins();
|
||||||
|
all.splice(all.findIndex(p => p.id === plugin.id), 1);
|
||||||
|
setLocalPlugins(all);
|
||||||
|
renderLocalPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
controls.appendChild(editBtn);
|
||||||
|
controls.appendChild(toggleBtn);
|
||||||
|
controls.appendChild(removeBtn);
|
||||||
|
}
|
||||||
|
row.appendChild(left);
|
||||||
|
row.appendChild(controls);
|
||||||
|
content.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleLocalInput(input) {
|
||||||
|
Object.assign(input.style, {
|
||||||
|
padding: "6px 8px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
background: "rgba(255,255,255,0.05)",
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: "13px"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleLocalBtn(btn, bg) {
|
||||||
|
Object.assign(btn.style, {
|
||||||
|
padding: "5px 12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: bg || "rgba(255,255,255,0.08)",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "12px",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
});
|
||||||
|
btn.onmouseenter = () => btn.style.opacity = "0.75";
|
||||||
|
btn.onmouseleave = () => btn.style.opacity = "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectLocalButton() {
|
||||||
|
if (document.getElementById("avia-local-plugins-btn")) return;
|
||||||
|
const appearanceBtn = [...document.querySelectorAll("a")]
|
||||||
|
.find(a => a.textContent.trim() === "Appearance");
|
||||||
|
if (!appearanceBtn) return;
|
||||||
|
|
||||||
|
const aviaPluginsBtn = document.getElementById("stoat-fake-plugins");
|
||||||
|
if (!aviaPluginsBtn) return;
|
||||||
|
|
||||||
|
const localBtn = appearanceBtn.cloneNode(true);
|
||||||
|
localBtn.id = "avia-local-plugins-btn";
|
||||||
|
|
||||||
|
const textNode = [...localBtn.querySelectorAll("div")]
|
||||||
|
.find(d => d.children.length === 0 && d.textContent.trim() === "Appearance");
|
||||||
|
if (textNode) textNode.textContent = "(Sanctum) Local Plugins";
|
||||||
|
|
||||||
|
const oldSvg = localBtn.querySelector("svg");
|
||||||
|
if (oldSvg) oldSvg.remove();
|
||||||
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
|
const svg = document.createElementNS(svgNS, "svg");
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("width", "20");
|
||||||
|
svg.setAttribute("height", "20");
|
||||||
|
svg.setAttribute("fill", "currentColor");
|
||||||
|
svg.style.marginRight = "8px";
|
||||||
|
const path = document.createElementNS(svgNS, "path");
|
||||||
|
|
||||||
|
path.setAttribute("d", "M20.5 11H19V7a2 2 0 00-2-2h-4V3.5a2.5 2.5 0 00-5 0V5H4a2 2 0 00-2 2v3.8h1.5c1.5 0 2.7 1.2 2.7 2.7S5 16.2 3.5 16.2H2V20a2 2 0 002 2h3.8v-1.5c0-1.5 1.2-2.7 2.7-2.7s2.7 1.2 2.7 2.7V22H17a2 2 0 002-2v-4h1.5a2.5 2.5 0 000-5z");
|
||||||
|
svg.appendChild(path);
|
||||||
|
localBtn.insertBefore(svg, localBtn.firstChild);
|
||||||
|
|
||||||
|
localBtn.addEventListener("click", toggleLocalPanel);
|
||||||
|
aviaPluginsBtn.parentElement.insertBefore(localBtn, aviaPluginsBtn.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForBody(callback) {
|
||||||
|
if (document.body) callback();
|
||||||
|
else new MutationObserver((obs) => {
|
||||||
|
if (document.body) { obs.disconnect(); callback(); }
|
||||||
|
}).observe(document.documentElement, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForBody(() => {
|
||||||
|
const observer = new MutationObserver(() => injectLocalButton());
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
injectLocalButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
upsertBuiltinLocalPlugins();
|
||||||
|
getLocalPlugins().forEach(plugin => {
|
||||||
|
if (plugin.enabled) runLocalPlugin(plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
preloadMonaco();
|
||||||
|
|
||||||
|
})();
|
||||||
139
avia_core/LoginWithToken.js
Normal file
139
avia_core/LoginWithToken.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__LOGIN_WITH_TOKEN__) return;
|
||||||
|
window.__LOGIN_WITH_TOKEN__ = true;
|
||||||
|
|
||||||
|
async function loginWithToken(token) {
|
||||||
|
const res = await fetch('https://stoat.chat/api/users/@me', {
|
||||||
|
headers: { 'x-session-token': token }
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Invalid token');
|
||||||
|
const user = await res.json();
|
||||||
|
|
||||||
|
const db = await new Promise((resolve, reject) => {
|
||||||
|
const r = indexedDB.open('localforage');
|
||||||
|
r.onsuccess = () => resolve(r.result);
|
||||||
|
r.onerror = () => reject(r.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tx = db.transaction('keyvaluepairs', 'readwrite');
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const r = tx.objectStore('keyvaluepairs').put({
|
||||||
|
session: {
|
||||||
|
_id: user._id,
|
||||||
|
token: token,
|
||||||
|
userId: user._id,
|
||||||
|
valid: true
|
||||||
|
}
|
||||||
|
}, 'auth');
|
||||||
|
r.onsuccess = () => resolve();
|
||||||
|
r.onerror = () => reject(r.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTokenDialog() {
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'top_0 left_0 right_0 bottom_0 pos_fixed z_100 max-h_100% d_grid us_none place-items_center pointer-events_all anim-n_scrimFadeIn anim-dur_0.1s anim-fm_forwards trs_var(--transitions-medium)_all p_80px ov-y_auto';
|
||||||
|
backdrop.style.cssText = '--background: rgba(0, 0, 0, 0.6);';
|
||||||
|
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div style="opacity: 1; --motion-translateY: 0px; transform: translateY(var(--motion-translateY));">
|
||||||
|
<div class="p_24px min-w_280px max-w_560px bdr_28px d_flex flex-d_column c_var(--md-sys-color-on-surface) bg_var(--md-sys-color-surface-container-high)">
|
||||||
|
<span class="lh_2rem fs_1.5rem ls_0 fw_400 mbe_16px">Login With Token</span>
|
||||||
|
<div class="c_var(--md-sys-color-on-surface-variant) lh_1.25rem fs_0.875rem ls_0.015625rem fw_400">
|
||||||
|
<div class="d_flex flex-d_column flex-g_initial m_0 ai_initial jc_initial gap_var(--gap-md)">
|
||||||
|
<mdui-text-field id="lwt-token-input" variant="filled" type="password" name="token" required label="Session Token"></mdui-text-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gap_8px d_flex jc_end mbs_24px">
|
||||||
|
<button id="lwt-close-btn" type="button" class="lh_1.25rem fs_0.875rem ls_0.015625rem fw_400 pos_relative px_16px flex-sh_0 d_flex ai_center jc_center ff_inherit cursor_pointer bd_none trs_var(--transitions-medium)_all c_var(--color) fill_var(--color) h_40px bdr_var(--borderRadius-full) --color_var(--md-sys-color-primary)">
|
||||||
|
<md-ripple aria-hidden="true"></md-ripple>Close
|
||||||
|
</button>
|
||||||
|
<button id="lwt-login-btn" type="button" class="lh_1.25rem fs_0.875rem ls_0.015625rem fw_400 pos_relative px_16px flex-sh_0 d_flex ai_center jc_center ff_inherit cursor_pointer bd_none trs_var(--transitions-medium)_all c_var(--color) fill_var(--color) h_40px bdr_var(--borderRadius-full) --color_var(--md-sys-color-on-primary) bg_var(--md-sys-color-primary)">
|
||||||
|
<md-ripple aria-hidden="true"></md-ripple>Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
const closeBtn = backdrop.querySelector('#lwt-close-btn');
|
||||||
|
const loginBtn = backdrop.querySelector('#lwt-login-btn');
|
||||||
|
const tokenInput = backdrop.querySelector('#lwt-token-input');
|
||||||
|
|
||||||
|
function close() { backdrop.remove(); }
|
||||||
|
|
||||||
|
function setLoading(loading) {
|
||||||
|
loginBtn.disabled = loading;
|
||||||
|
loginBtn.style.cursor = loading ? 'not-allowed' : 'pointer';
|
||||||
|
const ripple = loginBtn.querySelector('md-ripple');
|
||||||
|
loginBtn.textContent = loading ? 'Logging in…' : 'Login';
|
||||||
|
if (ripple) loginBtn.prepend(ripple);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setError(msg) {
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.style.cursor = 'pointer';
|
||||||
|
const ripple = loginBtn.querySelector('md-ripple');
|
||||||
|
loginBtn.textContent = msg;
|
||||||
|
if (ripple) loginBtn.prepend(ripple);
|
||||||
|
setTimeout(() => {
|
||||||
|
loginBtn.textContent = 'Login';
|
||||||
|
if (ripple) loginBtn.prepend(ripple);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
|
||||||
|
closeBtn.addEventListener('click', close);
|
||||||
|
|
||||||
|
loginBtn.addEventListener('click', async () => {
|
||||||
|
const token = tokenInput.value?.trim();
|
||||||
|
if (!token) {
|
||||||
|
setError('Enter a token!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await loginWithToken(token);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Invalid token!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectLoginButton() {
|
||||||
|
const signUpBtn = [...document.querySelectorAll('button')]
|
||||||
|
.find(b => b.textContent.trim() === 'Sign Up');
|
||||||
|
if (!signUpBtn) return;
|
||||||
|
|
||||||
|
const parent = signUpBtn.parentElement;
|
||||||
|
if (parent.querySelector('[data-lwt-btn]')) return;
|
||||||
|
|
||||||
|
const clone = signUpBtn.cloneNode(false);
|
||||||
|
clone.dataset.lwtBtn = 'true';
|
||||||
|
clone.textContent = 'Login With Token';
|
||||||
|
|
||||||
|
const ripple = document.createElement('md-ripple');
|
||||||
|
ripple.setAttribute('aria-hidden', 'true');
|
||||||
|
clone.prepend(ripple);
|
||||||
|
|
||||||
|
clone.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openTokenDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
signUpBtn.insertAdjacentElement('afterend', clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
let debounceTimer = null;
|
||||||
|
new MutationObserver(() => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(injectLoginButton, 150);
|
||||||
|
}).observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
injectLoginButton();
|
||||||
|
})();
|
||||||
605
avia_core/VCSounds.js
Normal file
605
avia_core/VCSounds.js
Normal file
|
|
@ -0,0 +1,605 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__VC_SOUNDS__) return;
|
||||||
|
window.__VC_SOUNDS__ = true;
|
||||||
|
|
||||||
|
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
if (ctx.state === "suspended") ctx.resume();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function playNote(freq, startTime, duration, volume) {
|
||||||
|
const osc1 = ctx.createOscillator();
|
||||||
|
const gain1 = ctx.createGain();
|
||||||
|
osc1.type = "sine";
|
||||||
|
osc1.frequency.value = freq;
|
||||||
|
osc1.connect(gain1);
|
||||||
|
gain1.connect(ctx.destination);
|
||||||
|
|
||||||
|
const osc2 = ctx.createOscillator();
|
||||||
|
const gain2 = ctx.createGain();
|
||||||
|
osc2.type = "triangle";
|
||||||
|
osc2.frequency.value = freq / 2;
|
||||||
|
osc2.connect(gain2);
|
||||||
|
gain2.connect(ctx.destination);
|
||||||
|
|
||||||
|
gain1.gain.setValueAtTime(0, startTime);
|
||||||
|
gain1.gain.linearRampToValueAtTime(volume, startTime + 0.03);
|
||||||
|
gain1.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
|
||||||
|
|
||||||
|
gain2.gain.setValueAtTime(0, startTime);
|
||||||
|
gain2.gain.linearRampToValueAtTime(volume * 0.4, startTime + 0.03);
|
||||||
|
gain2.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
|
||||||
|
|
||||||
|
osc1.start(startTime);
|
||||||
|
osc1.stop(startTime + duration + 0.05);
|
||||||
|
osc2.start(startTime);
|
||||||
|
osc2.stop(startTime + duration + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playJoin() {
|
||||||
|
if (ctx.state === "suspended") ctx.resume();
|
||||||
|
const t = ctx.currentTime + 0.01;
|
||||||
|
playNote(294, t, 0.35, 0.14);
|
||||||
|
playNote(370, t + 0.28, 0.45, 0.11);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playLeave() {
|
||||||
|
if (ctx.state === "suspended") ctx.resume();
|
||||||
|
const t = ctx.currentTime + 0.01;
|
||||||
|
playNote(370, t, 0.35, 0.14);
|
||||||
|
playNote(294, t + 0.28, 0.45, 0.11);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inVoice = false;
|
||||||
|
let initialising = false;
|
||||||
|
let initTimer = null;
|
||||||
|
let globalObserver = null;
|
||||||
|
let refreshTimer = null;
|
||||||
|
let lastVoiceState = null;
|
||||||
|
let lastMemberIdentityKey = "";
|
||||||
|
let leaveWatchdog = null;
|
||||||
|
const recentRowActivity = new Map();
|
||||||
|
|
||||||
|
function onSelfJoined() {
|
||||||
|
if (inVoice) return;
|
||||||
|
inVoice = true;
|
||||||
|
initialising = true;
|
||||||
|
playJoin();
|
||||||
|
console.debug("[VCSounds] self joined");
|
||||||
|
clearTimeout(initTimer);
|
||||||
|
initTimer = setTimeout(() => {
|
||||||
|
initialising = false;
|
||||||
|
}, 1500);
|
||||||
|
clearTimeout(leaveWatchdog);
|
||||||
|
leaveWatchdog = null;
|
||||||
|
publishVoiceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelfLeft() {
|
||||||
|
if (!inVoice) return;
|
||||||
|
inVoice = false;
|
||||||
|
initialising = false;
|
||||||
|
clearTimeout(initTimer);
|
||||||
|
clearTimeout(leaveWatchdog);
|
||||||
|
leaveWatchdog = null;
|
||||||
|
recentRowActivity.clear();
|
||||||
|
lastVoiceState = null;
|
||||||
|
lastMemberIdentityKey = "";
|
||||||
|
playLeave();
|
||||||
|
console.debug("[VCSounds] self left");
|
||||||
|
const overlayApi = window.native?.overlay;
|
||||||
|
if (overlayApi && typeof overlayApi.setVoiceState === "function") {
|
||||||
|
overlayApi.setVoiceState(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isParticipantEntry(el) {
|
||||||
|
if (el.nodeType !== 1) return false;
|
||||||
|
const c = String(el.className || "");
|
||||||
|
if (!el.isConnected) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width < 24 || rect.height < 24) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity || "1") <= 0.05) return false;
|
||||||
|
return (
|
||||||
|
c.includes("p_var(--gap-sm)") &&
|
||||||
|
c.includes("pos_relative") &&
|
||||||
|
c.includes("d_flex") &&
|
||||||
|
c.includes("ai_center")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findParticipantEntry(node) {
|
||||||
|
let current = node && node.nodeType === 1 ? node : node?.parentElement || null;
|
||||||
|
while (current) {
|
||||||
|
if (isParticipantEntry(current)) return current;
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRowActivity(node) {
|
||||||
|
const entry = findParticipantEntry(node);
|
||||||
|
if (!entry) return;
|
||||||
|
recentRowActivity.set(entry, Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneRowActivity() {
|
||||||
|
const cutoff = Date.now() - 15000;
|
||||||
|
for (const [entry, at] of recentRowActivity.entries()) {
|
||||||
|
if (typeof at !== "number" || at < cutoff || !entry.isConnected) {
|
||||||
|
recentRowActivity.delete(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pseudoStyleIsVisible(style) {
|
||||||
|
if (!style) return false;
|
||||||
|
const content = String(style.content || "").toLowerCase();
|
||||||
|
if (content === "none") return false;
|
||||||
|
return (
|
||||||
|
style.display !== "none" &&
|
||||||
|
style.visibility !== "hidden" &&
|
||||||
|
parseFloat(style.opacity || "1") > 0.05
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pseudoLooksActive(style) {
|
||||||
|
if (!pseudoStyleIsVisible(style)) return false;
|
||||||
|
|
||||||
|
const boxShadow = String(style.boxShadow || "").toLowerCase();
|
||||||
|
const outlineWidth = parseFloat(style.outlineWidth || "0");
|
||||||
|
const borderWidth = parseFloat(style.borderWidth || "0");
|
||||||
|
const filter = String(style.filter || "").toLowerCase();
|
||||||
|
const transform = String(style.transform || "").toLowerCase();
|
||||||
|
const background = String(style.backgroundColor || "").toLowerCase();
|
||||||
|
const borderColor = String(style.borderColor || "").toLowerCase();
|
||||||
|
const borderRadius = String(style.borderRadius || "").toLowerCase();
|
||||||
|
const size = Math.max(parseFloat(style.width || "0"), parseFloat(style.height || "0"));
|
||||||
|
|
||||||
|
const ringish =
|
||||||
|
boxShadow !== "none" ||
|
||||||
|
outlineWidth > 0 ||
|
||||||
|
borderWidth > 0 ||
|
||||||
|
filter !== "none" ||
|
||||||
|
transform !== "none";
|
||||||
|
const accentish =
|
||||||
|
background.includes("rgba") ||
|
||||||
|
background.includes("rgb(") ||
|
||||||
|
borderColor.includes("rgba") ||
|
||||||
|
borderColor.includes("rgb(");
|
||||||
|
const circular = borderRadius.includes("50%") || borderRadius.includes("999");
|
||||||
|
|
||||||
|
return Boolean(size <= 80 && circular && (ringish || accentish));
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeVoiceActivityIndicator(node) {
|
||||||
|
if (!node || node.nodeType !== 1) return false;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
if (rect.width < 8 || rect.height < 8) return false;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(node);
|
||||||
|
if (!style || style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity || "1") <= 0.05) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = Math.min(rect.width, rect.height);
|
||||||
|
const borderRadius = String(style.borderRadius || "").toLowerCase();
|
||||||
|
const boxShadow = String(style.boxShadow || "").toLowerCase();
|
||||||
|
const outlineWidth = parseFloat(style.outlineWidth || "0");
|
||||||
|
const borderWidth = parseFloat(style.borderWidth || "0");
|
||||||
|
const filter = String(style.filter || "").toLowerCase();
|
||||||
|
const transform = String(style.transform || "").toLowerCase();
|
||||||
|
const animation = String(style.animationName || "").toLowerCase();
|
||||||
|
const transition = String(style.transitionProperty || "").toLowerCase();
|
||||||
|
const background = String(style.backgroundColor || "").toLowerCase();
|
||||||
|
const borderColor = String(style.borderColor || "").toLowerCase();
|
||||||
|
|
||||||
|
const circular = borderRadius.includes("50%") || borderRadius.includes("999");
|
||||||
|
const ringish =
|
||||||
|
boxShadow !== "none" ||
|
||||||
|
outlineWidth > 0 ||
|
||||||
|
borderWidth > 0 ||
|
||||||
|
filter !== "none" ||
|
||||||
|
transform !== "none" ||
|
||||||
|
animation !== "none" ||
|
||||||
|
transition !== "none";
|
||||||
|
const accentish =
|
||||||
|
background.includes("rgba") ||
|
||||||
|
background.includes("rgb(") ||
|
||||||
|
borderColor.includes("rgba") ||
|
||||||
|
borderColor.includes("rgb(");
|
||||||
|
|
||||||
|
if (size <= 80 && circular && (ringish || accentish)) return true;
|
||||||
|
|
||||||
|
const before = window.getComputedStyle(node, "::before");
|
||||||
|
const after = window.getComputedStyle(node, "::after");
|
||||||
|
if (pseudoLooksActive(before) || pseudoLooksActive(after)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startObserver() {
|
||||||
|
if (globalObserver) return;
|
||||||
|
globalObserver = new MutationObserver((mutations) => {
|
||||||
|
if (!inVoice || initialising) return;
|
||||||
|
for (const m of mutations) {
|
||||||
|
markRowActivity(m.target);
|
||||||
|
for (const node of m.addedNodes) {
|
||||||
|
markRowActivity(node);
|
||||||
|
if (isParticipantEntry(node)) {
|
||||||
|
console.debug("[VCSounds] participant joined");
|
||||||
|
playJoin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const node of m.removedNodes) {
|
||||||
|
markRowActivity(node);
|
||||||
|
if (isParticipantEntry(node)) {
|
||||||
|
console.debug("[VCSounds] participant left");
|
||||||
|
playLeave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publishVoiceState();
|
||||||
|
});
|
||||||
|
globalObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
globalObserver.observe(document.body, { attributes: true, subtree: true, attributeFilter: ["class", "aria-label", "style", "data-speaking", "data-active", "data-state", "title"] });
|
||||||
|
if (!refreshTimer) {
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
if (inVoice && !initialising) publishVoiceState();
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParticipantName(entry) {
|
||||||
|
const text = (entry.textContent || "").replace(/\s+/g, " ").trim();
|
||||||
|
if (!text) return "Unknown";
|
||||||
|
return text.length > 40 ? text.slice(0, 40) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpeakingEntry(entry) {
|
||||||
|
const recentActivityAt = recentRowActivity.get(entry);
|
||||||
|
const recentActivity = typeof recentActivityAt === "number" && Date.now() - recentActivityAt < 1200;
|
||||||
|
const nodes = [entry, ...Array.from(entry.querySelectorAll("*"))];
|
||||||
|
for (const node of nodes) {
|
||||||
|
const className = String(node.className || "").toLowerCase();
|
||||||
|
const label = String(node.getAttribute?.("aria-label") || "").toLowerCase();
|
||||||
|
const title = String(node.getAttribute?.("title") || "").toLowerCase();
|
||||||
|
const text = String(node.textContent || "").toLowerCase();
|
||||||
|
const state = String(node.getAttribute?.("data-state") || "").toLowerCase();
|
||||||
|
const active = String(node.getAttribute?.("data-active") || "").toLowerCase();
|
||||||
|
const speaking = String(node.getAttribute?.("data-speaking") || "").toLowerCase();
|
||||||
|
const style = String(node.getAttribute?.("style") || "").toLowerCase();
|
||||||
|
const attrs = typeof node.getAttributeNames === "function" ? node.getAttributeNames().map((name) => name.toLowerCase()) : [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
className.includes("speaking") ||
|
||||||
|
className.includes("voice-activity") ||
|
||||||
|
className.includes("active-speaker") ||
|
||||||
|
className.includes("active") ||
|
||||||
|
label.includes("speaking") ||
|
||||||
|
label.includes("active") ||
|
||||||
|
label.includes("voice activity") ||
|
||||||
|
title.includes("speaking") ||
|
||||||
|
title.includes("active") ||
|
||||||
|
text.includes("speaking") ||
|
||||||
|
text.includes("voice activity") ||
|
||||||
|
state === "speaking" ||
|
||||||
|
active === "true" ||
|
||||||
|
speaking === "true" ||
|
||||||
|
style.includes("speaking") ||
|
||||||
|
attrs.includes("data-speaking") ||
|
||||||
|
attrs.includes("data-active") ||
|
||||||
|
attrs.includes("data-state")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentActivity && nodes.some(looksLikeVoiceActivityIndicator)) return true;
|
||||||
|
if (nodes.some(looksLikeVoiceActivityIndicator)) return true;
|
||||||
|
|
||||||
|
return !!entry.querySelector(
|
||||||
|
"[data-speaking='true'], [data-active='true'], [data-state='speaking'], [aria-label*='speaking'], [aria-label*='voice activity'], [title*='speaking'], [class*='speaking'], [class*='active-speaker'], [class*='voice-activity'], [class*='active']",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAvatarUrl(entry) {
|
||||||
|
const candidates = Array.from(entry.querySelectorAll("img, source, [style*='background-image'], [style*='background']"));
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (candidate.tagName === "IMG") {
|
||||||
|
const src = candidate.currentSrc || candidate.src || candidate.getAttribute("src") || "";
|
||||||
|
if (src) return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = String(candidate.getAttribute("style") || "");
|
||||||
|
const match = style.match(/url\(["']?([^"')]+)["']?\)/i);
|
||||||
|
if (match && match[1]) return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = entry.querySelector("img");
|
||||||
|
if (img) {
|
||||||
|
const src = img.currentSrc || img.src || img.getAttribute("src") || "";
|
||||||
|
if (src) return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAvatarUrl(url) {
|
||||||
|
return String(url || "")
|
||||||
|
.replace(/\/original(?=$|[?#])/i, "")
|
||||||
|
.replace(/[?#].*$/, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAvatarLikeSrc(src) {
|
||||||
|
const value = String(src || "").toLowerCase();
|
||||||
|
return (
|
||||||
|
value.includes("/avatars/") ||
|
||||||
|
value.includes("/default_avatar") ||
|
||||||
|
value.includes("/avatar") ||
|
||||||
|
value.includes("/icons/") ||
|
||||||
|
value.includes("avatar")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleElement(el) {
|
||||||
|
if (!el || el.nodeType !== 1) return false;
|
||||||
|
if (!el.isConnected) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width < 8 || rect.height < 8) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
if (!style) return false;
|
||||||
|
return style.display !== "none" && style.visibility !== "hidden" && parseFloat(style.opacity || "1") > 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAvatarRoot(img) {
|
||||||
|
let current = img && img.parentElement;
|
||||||
|
let depth = 0;
|
||||||
|
while (current && depth < 6) {
|
||||||
|
if (!isVisibleElement(current)) {
|
||||||
|
current = current.parentElement;
|
||||||
|
depth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = current.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(current);
|
||||||
|
const radius = String(style.borderRadius || "").toLowerCase();
|
||||||
|
const clip = String(style.clipPath || "").toLowerCase();
|
||||||
|
const circular = radius.includes("50%") || radius.includes("999") || clip.includes("circle");
|
||||||
|
if (circular || (rect.width >= 24 && rect.height >= 24 && rect.width <= 240 && rect.height <= 240)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.parentElement;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return img?.parentElement || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSpeakingAvatarUrls() {
|
||||||
|
const speaking = new Set();
|
||||||
|
const ringTiles = Array.from(document.querySelectorAll("*")).filter((el) => {
|
||||||
|
const cls = String(el.className || "");
|
||||||
|
return (
|
||||||
|
el.isConnected &&
|
||||||
|
cls.includes("vc_tile") &&
|
||||||
|
cls.includes("ring-c_var(--md-sys-color-primary)")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const tile of ringTiles) {
|
||||||
|
const imageNodes = Array.from(tile.querySelectorAll("img")).filter((img) => isVisibleElement(img));
|
||||||
|
for (const img of imageNodes) {
|
||||||
|
const src = normalizeAvatarUrl(img.currentSrc || img.src || img.getAttribute("src") || "");
|
||||||
|
if (src && isAvatarLikeSrc(src)) {
|
||||||
|
speaking.add(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = Array.from(document.querySelectorAll("img")).filter((img) => {
|
||||||
|
if (!isVisibleElement(img)) return false;
|
||||||
|
const src = normalizeAvatarUrl(img.currentSrc || img.src || img.getAttribute("src") || "");
|
||||||
|
return isAvatarLikeSrc(src);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const img of images) {
|
||||||
|
const src = normalizeAvatarUrl(img.currentSrc || img.src || img.getAttribute("src") || "");
|
||||||
|
const root = findAvatarRoot(img);
|
||||||
|
if (!root) continue;
|
||||||
|
|
||||||
|
const nodes = [root, ...Array.from(root.querySelectorAll("*"))].slice(0, 40);
|
||||||
|
const speaks = nodes.some(looksLikeVoiceActivityIndicator) || nodes.some(isSpeakingMarkerNode);
|
||||||
|
if (speaks) speaking.add(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
return speaking;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpeakingMarkerNode(node) {
|
||||||
|
if (!node || node.nodeType !== 1) return false;
|
||||||
|
const className = String(node.className || "").toLowerCase();
|
||||||
|
const label = String(node.getAttribute?.("aria-label") || "").toLowerCase();
|
||||||
|
const title = String(node.getAttribute?.("title") || "").toLowerCase();
|
||||||
|
const text = String(node.textContent || "").toLowerCase();
|
||||||
|
const state = String(node.getAttribute?.("data-state") || "").toLowerCase();
|
||||||
|
const active = String(node.getAttribute?.("data-active") || "").toLowerCase();
|
||||||
|
const speaking = String(node.getAttribute?.("data-speaking") || "").toLowerCase();
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
className.includes("speaking") ||
|
||||||
|
className.includes("active-speaker") ||
|
||||||
|
className.includes("voice-activity") ||
|
||||||
|
label.includes("speaking") ||
|
||||||
|
label.includes("voice activity") ||
|
||||||
|
title.includes("speaking") ||
|
||||||
|
title.includes("voice activity") ||
|
||||||
|
text.includes("speaking") ||
|
||||||
|
text.includes("voice activity") ||
|
||||||
|
state === "speaking" ||
|
||||||
|
active === "true" ||
|
||||||
|
speaking === "true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectSelfFlags() {
|
||||||
|
const buttons = Array.from(document.querySelectorAll("button"));
|
||||||
|
const muteButton = buttons.find((button) =>
|
||||||
|
/unmute|mute/i.test(
|
||||||
|
button.getAttribute("aria-label") || button.title || button.textContent || "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const deafenButton = buttons.find((button) =>
|
||||||
|
/undeafen|deafen/i.test(
|
||||||
|
button.getAttribute("aria-label") || button.title || button.textContent || "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
selfMuted: muteButton
|
||||||
|
? /unmute/i.test(
|
||||||
|
muteButton.getAttribute("aria-label") ||
|
||||||
|
muteButton.title ||
|
||||||
|
muteButton.textContent ||
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
selfDeafened: deafenButton
|
||||||
|
? /undeafen/i.test(
|
||||||
|
deafenButton.getAttribute("aria-label") ||
|
||||||
|
deafenButton.title ||
|
||||||
|
deafenButton.textContent ||
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectVoiceState() {
|
||||||
|
const speakingAvatars = collectSpeakingAvatarUrls();
|
||||||
|
const members = Array.from(document.querySelectorAll("*"))
|
||||||
|
.filter(isParticipantEntry)
|
||||||
|
.slice(0, 10)
|
||||||
|
.map((entry) => ({
|
||||||
|
name: getParticipantName(entry),
|
||||||
|
speaking: isSpeakingEntry(entry),
|
||||||
|
avatarUrl: normalizeAvatarUrl(extractAvatarUrl(entry)) || undefined,
|
||||||
|
}))
|
||||||
|
.map((member) => ({
|
||||||
|
...member,
|
||||||
|
speaking:
|
||||||
|
member.speaking ||
|
||||||
|
(member.avatarUrl ? speakingAvatars.has(normalizeAvatarUrl(member.avatarUrl)) : false),
|
||||||
|
}));
|
||||||
|
const selfFlags = detectSelfFlags();
|
||||||
|
return {
|
||||||
|
channelName: "Voice call",
|
||||||
|
// The join/leave hook is authoritative for whether the overlay should show.
|
||||||
|
// The DOM scan is only used to enrich the overlay with members while in call.
|
||||||
|
isInCall: inVoice,
|
||||||
|
members,
|
||||||
|
selfMuted: selfFlags.selfMuted,
|
||||||
|
selfDeafened: selfFlags.selfDeafened,
|
||||||
|
source: "voice DOM",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function memberIdentityKey(members) {
|
||||||
|
return members
|
||||||
|
.map((member) =>
|
||||||
|
[
|
||||||
|
String(member?.name || "").trim().toLowerCase(),
|
||||||
|
String(member?.avatarUrl || "").trim().toLowerCase(),
|
||||||
|
].join(":"),
|
||||||
|
)
|
||||||
|
.sort()
|
||||||
|
.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishVoiceState() {
|
||||||
|
const overlayApi = window.native?.overlay;
|
||||||
|
if (!overlayApi || typeof overlayApi.setVoiceState !== "function") return;
|
||||||
|
|
||||||
|
pruneRowActivity();
|
||||||
|
const next = collectVoiceState();
|
||||||
|
const voiceState = inVoice ? next : null;
|
||||||
|
const nextMemberKey = memberIdentityKey(next.members);
|
||||||
|
const nextKey = JSON.stringify(voiceState);
|
||||||
|
const memberChanged = nextMemberKey !== lastMemberIdentityKey;
|
||||||
|
if (memberChanged && inVoice && !initialising && lastMemberIdentityKey) {
|
||||||
|
const previousMembers = new Set(
|
||||||
|
lastMemberIdentityKey
|
||||||
|
.split("|")
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
const currentMembers = new Set(
|
||||||
|
nextMemberKey
|
||||||
|
.split("|")
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
const added = [...currentMembers].some((entry) => !previousMembers.has(entry));
|
||||||
|
const removed = [...previousMembers].some((entry) => !currentMembers.has(entry));
|
||||||
|
if (added) playJoin();
|
||||||
|
if (removed) playLeave();
|
||||||
|
}
|
||||||
|
lastMemberIdentityKey = nextMemberKey;
|
||||||
|
if (nextKey === lastVoiceState) return;
|
||||||
|
lastVoiceState = nextKey;
|
||||||
|
|
||||||
|
if (inVoice && next.members.length === 0) {
|
||||||
|
if (!leaveWatchdog) {
|
||||||
|
leaveWatchdog = setTimeout(() => {
|
||||||
|
leaveWatchdog = null;
|
||||||
|
if (!inVoice) return;
|
||||||
|
const stillEmpty = collectVoiceState().members.length === 0;
|
||||||
|
if (stillEmpty) {
|
||||||
|
overlayApi.setVoiceState(null);
|
||||||
|
onSelfLeft();
|
||||||
|
}
|
||||||
|
}, 2200);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearTimeout(leaveWatchdog);
|
||||||
|
leaveWatchdog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayApi.setVoiceState(voiceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = async function (...args) {
|
||||||
|
const url = typeof args[0] === "string" ? args[0] : args[0]?.url ?? "";
|
||||||
|
const response = await originalFetch.apply(this, args);
|
||||||
|
if (url.includes("/join_call") && response.ok) {
|
||||||
|
setTimeout(onSelfJoined, 300);
|
||||||
|
}
|
||||||
|
if (/(leave_call|leave-?call|disconnect|close_call)/i.test(url) && response.ok) {
|
||||||
|
setTimeout(onSelfLeft, 150);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OriginalWebSocket = window.WebSocket;
|
||||||
|
window.WebSocket = function (url, protocols) {
|
||||||
|
const ws = protocols
|
||||||
|
? new OriginalWebSocket(url, protocols)
|
||||||
|
: new OriginalWebSocket(url);
|
||||||
|
if (typeof url === "string" && url.includes("/livekit/rtc")) {
|
||||||
|
ws.addEventListener("close", () => onSelfLeft());
|
||||||
|
}
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
Object.assign(window.WebSocket, OriginalWebSocket);
|
||||||
|
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
||||||
|
|
||||||
|
startObserver();
|
||||||
|
publishVoiceState();
|
||||||
|
})();
|
||||||
34
avia_core/aviaclientcategory.js
Normal file
34
avia_core/aviaclientcategory.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
(function(){
|
||||||
|
if(window.__AVIA_CATEGORY_SETTINGS__) return;
|
||||||
|
window.__AVIA_CATEGORY_SETTINGS__ = true;
|
||||||
|
|
||||||
|
function inject(){
|
||||||
|
|
||||||
|
if(document.getElementById('avia-cloned-settings')) return;
|
||||||
|
|
||||||
|
const spans = [...document.querySelectorAll('span')];
|
||||||
|
const target = spans.find(s => s.textContent.trim() === "User Settings");
|
||||||
|
if(!target) return;
|
||||||
|
|
||||||
|
const container = target.closest('.d_flex.flex-d_column');
|
||||||
|
if(!container) return;
|
||||||
|
|
||||||
|
const clone = container.cloneNode(true);
|
||||||
|
clone.id = "avia-cloned-settings";
|
||||||
|
|
||||||
|
const header = clone.querySelector('span');
|
||||||
|
if(header) header.textContent = "AVIA CLIENT SETTINGS";
|
||||||
|
|
||||||
|
const list = clone.querySelector('.d_flex.flex-d_column.gap_var\\(--gap-s\\)');
|
||||||
|
if(list) list.innerHTML = "";
|
||||||
|
|
||||||
|
container.parentNode.insertBefore(clone, container.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
new MutationObserver(() => {
|
||||||
|
inject();
|
||||||
|
}).observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
inject();
|
||||||
|
|
||||||
|
})();
|
||||||
40
avia_core/aviadesktopversion.js
Normal file
40
avia_core/aviadesktopversion.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__AVIA_DESKTOP_PATCH__) return;
|
||||||
|
window.__AVIA_DESKTOP_PATCH__ = true;
|
||||||
|
|
||||||
|
function patchButton() {
|
||||||
|
document
|
||||||
|
.querySelectorAll("a.pos_relative.gap_16px.p_13px")
|
||||||
|
.forEach((el) => {
|
||||||
|
if (el.dataset.aviaPatched) return;
|
||||||
|
|
||||||
|
const textContainer = el.querySelector(
|
||||||
|
"div.d_flex.flex-g_1.flex-d_column",
|
||||||
|
);
|
||||||
|
if (!textContainer) return;
|
||||||
|
|
||||||
|
const nameDiv = textContainer.querySelector("div");
|
||||||
|
const versionSpan = textContainer.querySelector("span.lh_1rem");
|
||||||
|
|
||||||
|
if (!nameDiv || !versionSpan) return;
|
||||||
|
if (!nameDiv.textContent.includes("Stoat for Desktop")) return;
|
||||||
|
|
||||||
|
if (!versionSpan.textContent.includes("Version:")) return;
|
||||||
|
|
||||||
|
const aviaVersion = window.native.versions.aviaClient();
|
||||||
|
const stoatVersion = window.native.versions.desktop();
|
||||||
|
|
||||||
|
el.dataset.aviaPatched = "true";
|
||||||
|
|
||||||
|
nameDiv.textContent = "Sanctum Desktop";
|
||||||
|
versionSpan.textContent = `Version ${aviaVersion} (Based on Stoat ${stoatVersion})`;
|
||||||
|
|
||||||
|
textContainer.style.whiteSpace = "normal";
|
||||||
|
textContainer.style.overflow = "visible";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(patchButton);
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
patchButton();
|
||||||
|
})();
|
||||||
349
avia_core/aviafavsystem.js
Normal file
349
avia_core/aviafavsystem.js
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
if (window.__AVIA_FAVORITES_LOADED__) return;
|
||||||
|
window.__AVIA_FAVORITES_LOADED__ = true;
|
||||||
|
|
||||||
|
const STORAGE_KEY = "avia_favorites";
|
||||||
|
|
||||||
|
const getFavorites = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
|
const setFavorites = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
function extractYouTubeID(url) {
|
||||||
|
const reg = /(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^&?/]+)/;
|
||||||
|
const match = url.match(reg);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFavoritesPanel() {
|
||||||
|
|
||||||
|
let panel = document.getElementById("avia-favorites-panel");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = panel.style.display === "none" ? "flex" : "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "avia-favorites-panel";
|
||||||
|
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "40px",
|
||||||
|
right: "40px",
|
||||||
|
width: "640px",
|
||||||
|
height: "580px",
|
||||||
|
background: "#1e1e1e",
|
||||||
|
color: "#fff",
|
||||||
|
borderRadius: "20px",
|
||||||
|
boxShadow: "0 12px 35px rgba(0,0,0,0.45)",
|
||||||
|
zIndex: 999999,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.textContent = "Favorites";
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "18px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "16px",
|
||||||
|
background: "rgba(255,255,255,0.04)",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move",
|
||||||
|
position: "relative",
|
||||||
|
userSelect: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = document.createElement("div");
|
||||||
|
close.textContent = "✕";
|
||||||
|
Object.assign(close.style, {
|
||||||
|
position: "absolute",
|
||||||
|
right: "18px",
|
||||||
|
top: "16px",
|
||||||
|
cursor: "pointer"
|
||||||
|
});
|
||||||
|
close.onclick = () => panel.style.display = "none";
|
||||||
|
header.appendChild(close);
|
||||||
|
|
||||||
|
const inputRow = document.createElement("div");
|
||||||
|
Object.assign(inputRow.style, {
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
padding: "14px 18px"
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlInput = document.createElement("input");
|
||||||
|
urlInput.placeholder = "Paste link...";
|
||||||
|
Object.assign(urlInput.style, {
|
||||||
|
flex: "2",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
border: "none",
|
||||||
|
outline: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleInput = document.createElement("input");
|
||||||
|
titleInput.placeholder = "Optional title...";
|
||||||
|
Object.assign(titleInput.style, {
|
||||||
|
flex: "1",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
border: "none",
|
||||||
|
outline: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
const addBtn = document.createElement("button");
|
||||||
|
addBtn.textContent = "Add";
|
||||||
|
Object.assign(addBtn.style, {
|
||||||
|
padding: "10px 16px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer"
|
||||||
|
});
|
||||||
|
|
||||||
|
inputRow.appendChild(urlInput);
|
||||||
|
inputRow.appendChild(titleInput);
|
||||||
|
inputRow.appendChild(addBtn);
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
Object.assign(grid.style, {
|
||||||
|
flex: "1",
|
||||||
|
minHeight: "0",
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: "18px",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, 120px)",
|
||||||
|
gap: "14px",
|
||||||
|
alignContent: "start"
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(inputRow);
|
||||||
|
panel.appendChild(grid);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
let isDragging = false, offsetX, offsetY;
|
||||||
|
|
||||||
|
header.addEventListener("mousedown", e => {
|
||||||
|
isDragging = true;
|
||||||
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", () => isDragging = false);
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
panel.style.left = (e.clientX - offsetX) + "px";
|
||||||
|
panel.style.top = (e.clientY - offsetY) + "px";
|
||||||
|
panel.style.right = "auto";
|
||||||
|
panel.style.bottom = "auto";
|
||||||
|
});
|
||||||
|
|
||||||
|
function showToast(card) {
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.textContent = "Copied to clipboard";
|
||||||
|
Object.assign(toast.style, {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "6px",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
background: "rgba(0,0,0,0.85)",
|
||||||
|
padding: "6px 10px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
fontSize: "11px",
|
||||||
|
opacity: "0",
|
||||||
|
transition: "opacity 0.2s",
|
||||||
|
pointerEvents: "none"
|
||||||
|
});
|
||||||
|
card.appendChild(toast);
|
||||||
|
requestAnimationFrame(() => toast.style.opacity = "1");
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = "0";
|
||||||
|
setTimeout(() => toast.remove(), 200);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text) {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
try { document.execCommand("copy"); } catch {}
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
|
||||||
|
grid.innerHTML = "";
|
||||||
|
const favorites = getFavorites();
|
||||||
|
|
||||||
|
favorites.forEach(item => {
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
Object.assign(card.style, {
|
||||||
|
position: "relative",
|
||||||
|
width: "120px",
|
||||||
|
height: "120px",
|
||||||
|
borderRadius: "14px",
|
||||||
|
overflow: "hidden",
|
||||||
|
background: "rgba(255,255,255,0.05)",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center"
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = document.createElement("div");
|
||||||
|
remove.textContent = "✕";
|
||||||
|
Object.assign(remove.style, {
|
||||||
|
position: "absolute",
|
||||||
|
top: "6px",
|
||||||
|
right: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: "rgba(0,0,0,0.6)",
|
||||||
|
padding: "2px 6px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
zIndex: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
remove.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setFavorites(favorites.filter(f => f.url !== item.url));
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
card.appendChild(remove);
|
||||||
|
|
||||||
|
let mediaAdded = false;
|
||||||
|
|
||||||
|
const ytID = extractYouTubeID(item.url);
|
||||||
|
if (ytID) {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = `https://img.youtube.com/vi/${ytID}/hqdefault.jpg`;
|
||||||
|
Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" });
|
||||||
|
card.appendChild(img);
|
||||||
|
mediaAdded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mediaAdded) {
|
||||||
|
const ext = item.url.split(".").pop().split("?")[0].toLowerCase();
|
||||||
|
const isVideo = ["mp4","webm","mov","gifv"].includes(ext);
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.src = item.url.replace(".gifv",".mp4");
|
||||||
|
video.autoplay = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
Object.assign(video.style, { width:"100%", height:"100%", objectFit:"cover" });
|
||||||
|
video.onerror = fallback;
|
||||||
|
card.appendChild(video);
|
||||||
|
} else {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = item.url;
|
||||||
|
Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" });
|
||||||
|
img.onerror = fallback;
|
||||||
|
card.appendChild(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallback() {
|
||||||
|
card.innerHTML = "";
|
||||||
|
card.appendChild(remove);
|
||||||
|
const text = document.createElement("div");
|
||||||
|
text.textContent = item.title || item.url;
|
||||||
|
Object.assign(text.style, {
|
||||||
|
padding:"8px",
|
||||||
|
fontSize:"11px",
|
||||||
|
textAlign:"center",
|
||||||
|
wordBreak:"break-word"
|
||||||
|
});
|
||||||
|
card.appendChild(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.title) {
|
||||||
|
const titleOverlay = document.createElement("div");
|
||||||
|
titleOverlay.textContent = item.title;
|
||||||
|
Object.assign(titleOverlay.style, {
|
||||||
|
position:"absolute",
|
||||||
|
bottom:"0",
|
||||||
|
width:"100%",
|
||||||
|
background:"rgba(0,0,0,0.6)",
|
||||||
|
fontSize:"11px",
|
||||||
|
padding:"4px",
|
||||||
|
textAlign:"center",
|
||||||
|
whiteSpace:"nowrap",
|
||||||
|
overflow:"hidden",
|
||||||
|
textOverflow:"ellipsis"
|
||||||
|
});
|
||||||
|
card.appendChild(titleOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.onclick = () => {
|
||||||
|
const doToast = () => showToast(card);
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(item.url)
|
||||||
|
.then(doToast)
|
||||||
|
.catch(() => {
|
||||||
|
fallbackCopy(item.url);
|
||||||
|
doToast();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy(item.url);
|
||||||
|
doToast();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addBtn.onclick = () => {
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
const title = titleInput.value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
const favorites = getFavorites();
|
||||||
|
if (favorites.some(f => f.url === url)) return;
|
||||||
|
favorites.push({ url, title, addedAt: Date.now() });
|
||||||
|
setFavorites(favorites);
|
||||||
|
urlInput.value = "";
|
||||||
|
titleInput.value = "";
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectButton() {
|
||||||
|
|
||||||
|
if (document.getElementById("avia-favorites-btn")) return;
|
||||||
|
|
||||||
|
const gifSpan = [...document.querySelectorAll("span.material-symbols-outlined")]
|
||||||
|
.find(s => s.textContent.trim() === "gif");
|
||||||
|
|
||||||
|
if (!gifSpan) return;
|
||||||
|
|
||||||
|
const wrapper = gifSpan.closest("div.flex-sh_0");
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
const clone = wrapper.cloneNode(true);
|
||||||
|
clone.id = "avia-favorites-btn";
|
||||||
|
clone.querySelector("span.material-symbols-outlined").textContent = "star";
|
||||||
|
clone.querySelector("button").onclick = toggleFavoritesPanel;
|
||||||
|
|
||||||
|
wrapper.parentElement.insertBefore(clone, wrapper.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
new MutationObserver(injectButton)
|
||||||
|
.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
injectButton();
|
||||||
|
|
||||||
|
})();
|
||||||
34
avia_core/aviaversion.js
Normal file
34
avia_core/aviaversion.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__AVIA_VERSION_PATCH__) return;
|
||||||
|
window.__AVIA_VERSION_PATCH__ = true;
|
||||||
|
|
||||||
|
function patchVersion() {
|
||||||
|
document
|
||||||
|
.querySelectorAll("span.lh_1rem.fs_0\\.75rem.ls_0\\.03125rem.fw_500")
|
||||||
|
.forEach((el) => {
|
||||||
|
if (el.dataset.aviaPatched) return;
|
||||||
|
|
||||||
|
if (!el.textContent.trim().startsWith("Stoat for Desktop")) return;
|
||||||
|
|
||||||
|
const stoatVersion = window.native.versions.desktop();
|
||||||
|
|
||||||
|
el.dataset.aviaPatched = "true";
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
Sanctum Desktop<br>
|
||||||
|
<span style="font-size:10px;opacity:0.7;">
|
||||||
|
Based on Stoat ${stoatVersion}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(patchVersion);
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
patchVersion();
|
||||||
|
})();
|
||||||
175
avia_core/clientBackup.js
Normal file
175
avia_core/clientBackup.js
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__clientBackup) return;
|
||||||
|
window.__clientBackup = true;
|
||||||
|
|
||||||
|
const TARGET_TEXT = "Spellchecker";
|
||||||
|
const CLONE_KEY = "data-lsbackup-cloned";
|
||||||
|
|
||||||
|
function exportLS() {
|
||||||
|
const data = {};
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
data[key] = localStorage.getItem(key);
|
||||||
|
}
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "localstorage-backup.json";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importLS(file, onDone) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
let count = 0;
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
onDone(null, count);
|
||||||
|
} catch (err) {
|
||||||
|
onDone(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPanel() {
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.style.cssText = `
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--md-sys-color-surface-container-highest);
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const btnStyle = `
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const status = document.createElement("span");
|
||||||
|
status.style.cssText = "font-size: 11px; opacity: 0.7; min-height: 14px;";
|
||||||
|
|
||||||
|
const exportBtn = document.createElement("button");
|
||||||
|
exportBtn.textContent = "⬇ Export localStorage";
|
||||||
|
exportBtn.style.cssText = btnStyle + `
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
`;
|
||||||
|
exportBtn.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
exportLS();
|
||||||
|
status.textContent = `✓ Exported ${localStorage.length} keys`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.accept = ".json";
|
||||||
|
fileInput.style.cssText = "display: none;";
|
||||||
|
fileInput.addEventListener("change", e => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
importLS(file, (err, count) => {
|
||||||
|
if (err) {
|
||||||
|
status.textContent = "✗ Invalid JSON file";
|
||||||
|
} else {
|
||||||
|
status.textContent = `✓ Imported ${count} keys`;
|
||||||
|
}
|
||||||
|
fileInput.value = "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const importBtn = document.createElement("button");
|
||||||
|
importBtn.textContent = "⬆ Import localStorage";
|
||||||
|
importBtn.style.cssText = btnStyle + `
|
||||||
|
background: var(--md-sys-color-surface-container);
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant);
|
||||||
|
`;
|
||||||
|
importBtn.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.appendChild(exportBtn);
|
||||||
|
panel.appendChild(importBtn);
|
||||||
|
panel.appendChild(fileInput);
|
||||||
|
panel.appendChild(status);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryInject() {
|
||||||
|
document.querySelectorAll("a.pos_relative").forEach(btn => {
|
||||||
|
if (
|
||||||
|
btn.hasAttribute(CLONE_KEY) ||
|
||||||
|
btn.hasAttribute("data-lsbackup-entry") ||
|
||||||
|
!btn.innerText.includes(TARGET_TEXT)
|
||||||
|
) return;
|
||||||
|
|
||||||
|
btn.setAttribute(CLONE_KEY, "true");
|
||||||
|
|
||||||
|
const clone = btn.cloneNode(true);
|
||||||
|
clone.removeAttribute(CLONE_KEY);
|
||||||
|
clone.setAttribute("data-lsbackup-entry", "true");
|
||||||
|
|
||||||
|
const title = clone.querySelector("div.d_flex.flex-g_1.flex-d_column > div");
|
||||||
|
if (title) title.textContent = "Sanctum Backup";
|
||||||
|
|
||||||
|
const desc = clone.querySelector("div.d_flex.flex-g_1.flex-d_column > span");
|
||||||
|
if (desc) desc.textContent = "Backup or Restore all client data";
|
||||||
|
|
||||||
|
const iconBtn = document.createElement("div");
|
||||||
|
iconBtn.title = "LocalStorage Backup";
|
||||||
|
iconBtn.style.cssText = "cursor: pointer; z-index: 10; flex-shrink: 0;";
|
||||||
|
iconBtn.innerHTML = `
|
||||||
|
<div class="fill_var(--md-sys-color-on-surface) bg_var(--md-sys-color-surface-dim) w_36px h_36px d_flex flex-sh_0 ai_center jc_center bdr_var(--borderRadius-full)">
|
||||||
|
<span aria-hidden="true" class="material-symbols-outlined fs_inherit fw_undefined!" style="display: block; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0;">database</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existingIcon = clone.querySelector("div.fill_var\\(--md-sys-color-on-surface\\)");
|
||||||
|
if (existingIcon) {
|
||||||
|
existingIcon.replaceWith(iconBtn);
|
||||||
|
} else {
|
||||||
|
clone.prepend(iconBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
panel.style.display = panel.style.display === "flex" ? "none" : "flex";
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.style.cssText = "display: flex; flex-direction: column;";
|
||||||
|
|
||||||
|
const panel = buildPanel();
|
||||||
|
|
||||||
|
wrapper.appendChild(clone);
|
||||||
|
wrapper.appendChild(panel);
|
||||||
|
|
||||||
|
btn.parentNode.insertBefore(wrapper, btn.nextSibling);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryInject();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => tryInject());
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
57
avia_core/customFrameNativeMenu.js
Normal file
57
avia_core/customFrameNativeMenu.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__customFrameNativeMenu) return;
|
||||||
|
window.__customFrameNativeMenu = true;
|
||||||
|
|
||||||
|
function toggleCheckbox(elem, toggle) {
|
||||||
|
const checkbox = elem.querySelector("mdui-checkbox");
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
if (toggle) {
|
||||||
|
checkbox.setAttribute('checked', '');
|
||||||
|
checkbox.setAttribute('value', 'on');
|
||||||
|
} else {
|
||||||
|
checkbox.removeAttribute('checked');
|
||||||
|
checkbox.setAttribute('value', 'off');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCFNM() {
|
||||||
|
let elem = document.querySelector("#floating div:not(:empty) div.will-change_transform.flex_1_1_800px div:has(> a) > a:last-child");
|
||||||
|
if (!elem) { return; }
|
||||||
|
|
||||||
|
let title = elem.querySelector("div.flex-g_1 > div");
|
||||||
|
if (!title || title.textContent.trim() !== "Custom window frame") { return; }
|
||||||
|
|
||||||
|
let desc = elem.querySelector("div.flex-g_1 > span");
|
||||||
|
if (!desc || desc.textContent.trim() !== "Let Stoat use its own custom titlebar.") { return; }
|
||||||
|
|
||||||
|
var newElem = elem.cloneNode(true);
|
||||||
|
let nTitle = newElem.querySelector("div.flex-g_1 > div");
|
||||||
|
let nDesc = newElem.querySelector("div.flex-g_1 > span");
|
||||||
|
if (!nTitle || !nDesc) { newElem = null; return; }
|
||||||
|
|
||||||
|
nTitle.textContent = "Native menu on custom window frame";
|
||||||
|
nDesc.textContent = "Use the system's native menu on the custom window frame.";
|
||||||
|
|
||||||
|
let config = window.desktopConfig.get();
|
||||||
|
toggleCheckbox(newElem, config.customFrameNativeMenu);
|
||||||
|
|
||||||
|
newElem.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let config = window.desktopConfig.get();
|
||||||
|
config.customFrameNativeMenu = !config.customFrameNativeMenu;
|
||||||
|
window.desktopConfig.set(config);
|
||||||
|
|
||||||
|
toggleCheckbox(newElem, config.customFrameNativeMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.parentNode.appendChild(newElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
initCFNM();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => initCFNM());
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
66
avia_core/disableTrayIcon.js
Normal file
66
avia_core/disableTrayIcon.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__disableTrayClick) return;
|
||||||
|
window.__disableTrayClick = true;
|
||||||
|
|
||||||
|
function toggleCheckbox(elem, value) {
|
||||||
|
const checkbox = elem.querySelector("mdui-checkbox");
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
checkbox.setAttribute("checked", "");
|
||||||
|
checkbox.setAttribute("value", "on");
|
||||||
|
} else {
|
||||||
|
checkbox.removeAttribute("checked");
|
||||||
|
checkbox.setAttribute("value", "off");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButton(baseElem) {
|
||||||
|
const newElem = baseElem.cloneNode(true);
|
||||||
|
|
||||||
|
newElem.setAttribute("data-disable-tray-click", "true");
|
||||||
|
|
||||||
|
const title = newElem.querySelector("div.d_flex.flex-g_1 > div");
|
||||||
|
const desc = newElem.querySelector("div.d_flex.flex-g_1 > span");
|
||||||
|
const icon = newElem.querySelector("div.w_36px span.material-symbols-outlined");
|
||||||
|
|
||||||
|
if (title) title.textContent = "Disable Tray Icon Click";
|
||||||
|
if (desc) desc.textContent = "Prevents tray icon from toggling the app window.";
|
||||||
|
if (icon) icon.textContent = "block";
|
||||||
|
|
||||||
|
let config = window.desktopConfig.get();
|
||||||
|
toggleCheckbox(newElem, config.disableTrayClick);
|
||||||
|
|
||||||
|
newElem.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let config = window.desktopConfig.get();
|
||||||
|
config.disableTrayClick = !config.disableTrayClick;
|
||||||
|
window.desktopConfig.set(config);
|
||||||
|
|
||||||
|
toggleCheckbox(newElem, config.disableTrayClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newElem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectButton() {
|
||||||
|
const base = Array.from(document.querySelectorAll("a")).find((e) => {
|
||||||
|
const t = e.querySelector("div.d_flex.flex-g_1 > div");
|
||||||
|
return t && t.textContent.trim() === "Discord RPC";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!base) return;
|
||||||
|
if (document.querySelector("[data-disable-tray-click]")) return;
|
||||||
|
|
||||||
|
const newButton = createButton(base);
|
||||||
|
base.parentNode.appendChild(newButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
injectButton();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => injectButton());
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
397
avia_core/gamePresenceSettings.js
Normal file
397
avia_core/gamePresenceSettings.js
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__sanctumGamePresenceSettings) return;
|
||||||
|
window.__sanctumGamePresenceSettings = true;
|
||||||
|
|
||||||
|
const CLONE_ATTR = "data-sanctum-game-presence";
|
||||||
|
const PANEL_ATTR = "data-sanctum-game-presence-panel";
|
||||||
|
const POPULAR_GAMES = [
|
||||||
|
"Apex Legends",
|
||||||
|
"Among Us",
|
||||||
|
"Assassin's Creed Mirage",
|
||||||
|
"Assassin's Creed Valhalla",
|
||||||
|
"Armored Core VI: Fires of Rubicon",
|
||||||
|
"Baldur's Gate 3",
|
||||||
|
"Black Myth: Wukong",
|
||||||
|
"Brawlhalla",
|
||||||
|
"Call of Duty: Black Ops 6",
|
||||||
|
"Call of Duty: Modern Warfare III",
|
||||||
|
"Call of Duty: Warzone",
|
||||||
|
"Celeste",
|
||||||
|
"Cities: Skylines II",
|
||||||
|
"Civilization VI",
|
||||||
|
"Counter-Strike 2",
|
||||||
|
"Cuphead",
|
||||||
|
"Cyberpunk 2077",
|
||||||
|
"Dark Souls III",
|
||||||
|
"Dave the Diver",
|
||||||
|
"Days Gone",
|
||||||
|
"Dead by Daylight",
|
||||||
|
"Dead Cells",
|
||||||
|
"Deep Rock Galactic",
|
||||||
|
"Destiny 2",
|
||||||
|
"Diablo IV",
|
||||||
|
"Dota 2",
|
||||||
|
"Dragon's Dogma 2",
|
||||||
|
"Elden Ring",
|
||||||
|
"Enshrouded",
|
||||||
|
"Escape from Tarkov",
|
||||||
|
"Euro Truck Simulator 2",
|
||||||
|
"EVE Online",
|
||||||
|
"Fall Guys",
|
||||||
|
"Fallout 4",
|
||||||
|
"Fallout 76",
|
||||||
|
"Factorio",
|
||||||
|
"F1 24",
|
||||||
|
"Final Fantasy XIV",
|
||||||
|
"Forza Horizon 5",
|
||||||
|
"Fortnite",
|
||||||
|
"Genshin Impact",
|
||||||
|
"Ghost of Tsushima",
|
||||||
|
"God of War",
|
||||||
|
"Grand Theft Auto V",
|
||||||
|
"Grounded",
|
||||||
|
"Guild Wars 2",
|
||||||
|
"Hades",
|
||||||
|
"Hades II",
|
||||||
|
"Helldivers 2",
|
||||||
|
"Hogwarts Legacy",
|
||||||
|
"Hollow Knight",
|
||||||
|
"Honkai: Star Rail",
|
||||||
|
"Honkai Impact 3rd",
|
||||||
|
"Hunt: Showdown",
|
||||||
|
"It Takes Two",
|
||||||
|
"Kingdom Come: Deliverance",
|
||||||
|
"League of Legends",
|
||||||
|
"Lethal Company",
|
||||||
|
"Left 4 Dead 2",
|
||||||
|
"Last Epoch",
|
||||||
|
"Marvel Rivals",
|
||||||
|
"Minecraft",
|
||||||
|
"Monster Hunter: World",
|
||||||
|
"Monster Hunter Rise",
|
||||||
|
"Mortal Kombat 1",
|
||||||
|
"Metaphor: ReFantazio",
|
||||||
|
"No Man's Sky",
|
||||||
|
"Once Human",
|
||||||
|
"Overwatch 2",
|
||||||
|
"Palworld",
|
||||||
|
"Path of Exile",
|
||||||
|
"Path of Exile 2",
|
||||||
|
"Persona 5 Royal",
|
||||||
|
"Phasmophobia",
|
||||||
|
"PUBG: Battlegrounds",
|
||||||
|
"Paladins",
|
||||||
|
"Rainbow Six Siege",
|
||||||
|
"Red Dead Redemption 2",
|
||||||
|
"Resident Evil 4",
|
||||||
|
"Resident Evil Village",
|
||||||
|
"Rocket League",
|
||||||
|
"Rust",
|
||||||
|
"Satisfactory",
|
||||||
|
"Sea of Thieves",
|
||||||
|
"Skyrim Special Edition",
|
||||||
|
"Slay the Spire",
|
||||||
|
"Sons of the Forest",
|
||||||
|
"Spider-Man Remastered",
|
||||||
|
"Split Fiction",
|
||||||
|
"Star Citizen",
|
||||||
|
"Starfield",
|
||||||
|
"Stardew Valley",
|
||||||
|
"Street Fighter 6",
|
||||||
|
"Subnautica",
|
||||||
|
"Team Fortress 2",
|
||||||
|
"Tekken 8",
|
||||||
|
"Terraria",
|
||||||
|
"The Elder Scrolls Online",
|
||||||
|
"The Finals",
|
||||||
|
"The Last of Us Part I",
|
||||||
|
"The Witcher 3",
|
||||||
|
"Titanfall 2",
|
||||||
|
"VALORANT",
|
||||||
|
"V Rising",
|
||||||
|
"Valheim",
|
||||||
|
"Warframe",
|
||||||
|
"War Thunder",
|
||||||
|
"Wuthering Waves",
|
||||||
|
"World of Warcraft",
|
||||||
|
"World of Tanks",
|
||||||
|
"World of Warships",
|
||||||
|
"Zenless Zone Zero",
|
||||||
|
];
|
||||||
|
|
||||||
|
function toggleCheckbox(elem, value) {
|
||||||
|
const checkbox = elem.querySelector("mdui-checkbox");
|
||||||
|
if (!checkbox) return;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
checkbox.setAttribute("checked", "");
|
||||||
|
checkbox.setAttribute("value", "on");
|
||||||
|
} else {
|
||||||
|
checkbox.removeAttribute("checked");
|
||||||
|
checkbox.setAttribute("value", "off");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
return window.desktopConfig.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfig(next) {
|
||||||
|
window.desktopConfig.set(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPanel() {
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.setAttribute(PANEL_ATTR, "true");
|
||||||
|
panel.style.cssText = `
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
color: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const note = document.createElement("div");
|
||||||
|
note.style.cssText = "font-size:12px; opacity:0.75; line-height:1.35;";
|
||||||
|
note.textContent =
|
||||||
|
"Sanctum only lights up for games in its built-in catalog or names you add below.";
|
||||||
|
panel.appendChild(note);
|
||||||
|
|
||||||
|
const allowLabel = document.createElement("div");
|
||||||
|
allowLabel.textContent = "Allowed games / windows";
|
||||||
|
allowLabel.style.cssText = "font-size:12px; font-weight:600;";
|
||||||
|
panel.appendChild(allowLabel);
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = getConfig().gamePresenceAllowList || "";
|
||||||
|
textarea.rows = 4;
|
||||||
|
textarea.placeholder = "Examples: Fortnite, Valorant, Counter-Strike 2, Baldur's Gate 3";
|
||||||
|
textarea.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 88px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
background: rgba(0,0,0,0.18);
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
`;
|
||||||
|
textarea.addEventListener("input", () => {
|
||||||
|
const config = getConfig();
|
||||||
|
config.gamePresenceAllowList = textarea.value;
|
||||||
|
setConfig(config);
|
||||||
|
});
|
||||||
|
panel.appendChild(textarea);
|
||||||
|
|
||||||
|
const pickerLabel = document.createElement("div");
|
||||||
|
pickerLabel.textContent = "Popular games";
|
||||||
|
pickerLabel.style.cssText = "font-size:12px; font-weight:600; margin-top:2px;";
|
||||||
|
panel.appendChild(pickerLabel);
|
||||||
|
|
||||||
|
const pickerHint = document.createElement("div");
|
||||||
|
pickerHint.textContent = "Search and add games from the built-in catalog.";
|
||||||
|
pickerHint.style.cssText = "font-size:11px; opacity:0.7; line-height:1.35;";
|
||||||
|
panel.appendChild(pickerHint);
|
||||||
|
|
||||||
|
const pickerRow = document.createElement("div");
|
||||||
|
pickerRow.style.cssText = "display:flex; gap:8px; align-items:center;";
|
||||||
|
|
||||||
|
const pickerSearch = document.createElement("input");
|
||||||
|
pickerSearch.type = "search";
|
||||||
|
pickerSearch.placeholder = "Search popular games";
|
||||||
|
pickerSearch.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
background: rgba(0,0,0,0.18);
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const pickerAdd = document.createElement("button");
|
||||||
|
pickerAdd.type = "button";
|
||||||
|
pickerAdd.textContent = "Add";
|
||||||
|
pickerAdd.style.cssText = `
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
pickerRow.appendChild(pickerSearch);
|
||||||
|
pickerRow.appendChild(pickerAdd);
|
||||||
|
panel.appendChild(pickerRow);
|
||||||
|
|
||||||
|
const pickerList = document.createElement("div");
|
||||||
|
pickerList.style.cssText = `
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 2px;
|
||||||
|
`;
|
||||||
|
panel.appendChild(pickerList);
|
||||||
|
|
||||||
|
function existingEntries() {
|
||||||
|
return textarea.value
|
||||||
|
.split(/[\n,]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGameToAllowList(name) {
|
||||||
|
const current = new Set(existingEntries().map((item) => item.toLowerCase()));
|
||||||
|
if (current.has(name.toLowerCase())) return;
|
||||||
|
const next = existingEntries();
|
||||||
|
next.push(name);
|
||||||
|
textarea.value = next.join("\n");
|
||||||
|
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPicker() {
|
||||||
|
const query = pickerSearch.value.trim().toLowerCase();
|
||||||
|
const selected = new Set(existingEntries().map((item) => item.toLowerCase()));
|
||||||
|
pickerList.innerHTML = "";
|
||||||
|
|
||||||
|
const matches = POPULAR_GAMES.filter((game) => !query || game.toLowerCase().includes(query)).slice(0, 40);
|
||||||
|
for (const game of matches) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.textContent = selected.has(game.toLowerCase()) ? `✓ ${game}` : game;
|
||||||
|
button.title = selected.has(game.toLowerCase()) ? "Already added" : `Add ${game}`;
|
||||||
|
button.style.cssText = `
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid ${selected.has(game.toLowerCase()) ? "rgba(104, 126, 255, 0.55)" : "rgba(255,255,255,0.12)"};
|
||||||
|
background: ${selected.has(game.toLowerCase()) ? "rgba(104, 126, 255, 0.16)" : "rgba(255,255,255,0.05)"};
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
button.addEventListener("click", () => addGameToAllowList(game));
|
||||||
|
pickerList.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pickerSearch.addEventListener("input", renderPicker);
|
||||||
|
pickerAdd.addEventListener("click", () => {
|
||||||
|
const query = pickerSearch.value.trim();
|
||||||
|
if (!query) return;
|
||||||
|
const exact = POPULAR_GAMES.find((game) => game.toLowerCase() === query.toLowerCase());
|
||||||
|
if (exact) {
|
||||||
|
addGameToAllowList(exact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addGameToAllowList(query);
|
||||||
|
});
|
||||||
|
textarea.addEventListener("input", renderPicker);
|
||||||
|
renderPicker();
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButton(baseElem) {
|
||||||
|
const row = baseElem.cloneNode(true);
|
||||||
|
row.setAttribute(CLONE_ATTR, "true");
|
||||||
|
|
||||||
|
const title = row.querySelector("div.d_flex.flex-g_1 > div");
|
||||||
|
const desc = row.querySelector("div.d_flex.flex-g_1 > span");
|
||||||
|
const icon = row.querySelector("div.w_36px span.material-symbols-outlined");
|
||||||
|
const existingIcon = row.querySelector("div.w_36px");
|
||||||
|
|
||||||
|
if (title) title.textContent = "Gameplay overlay";
|
||||||
|
if (desc) desc.textContent = "Shows the mini voice overlay while you are in a game.";
|
||||||
|
if (icon) icon.textContent = "sports_esports";
|
||||||
|
|
||||||
|
if (existingIcon) {
|
||||||
|
existingIcon.title = "Toggle gameplay sharing settings";
|
||||||
|
existingIcon.style.cursor = "pointer";
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsBtn = document.createElement("div");
|
||||||
|
settingsBtn.title = "Edit gameplay sharing";
|
||||||
|
settingsBtn.style.cssText = "cursor: pointer; z-index: 10; flex-shrink: 0; margin-left: 6px;";
|
||||||
|
settingsBtn.innerHTML = `
|
||||||
|
<div class="fill_var(--md-sys-color-on-surface) bg_var(--md-sys-color-surface-dim) w_36px h_36px d_flex flex-sh_0 ai_center jc_center bdr_var(--borderRadius-full)">
|
||||||
|
<span aria-hidden="true" class="material-symbols-outlined fs_inherit fw_undefined!" style="display:block;font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0;">settings</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iconSlot = row.querySelector(".d_flex.ai_center.jc_center, .w_36px");
|
||||||
|
if (iconSlot && iconSlot.parentNode) {
|
||||||
|
iconSlot.parentNode.appendChild(settingsBtn);
|
||||||
|
} else {
|
||||||
|
row.appendChild(settingsBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = buildPanel();
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.style.cssText = "display:flex; flex-direction:column;";
|
||||||
|
|
||||||
|
const applyState = () => {
|
||||||
|
const config = getConfig();
|
||||||
|
toggleCheckbox(row, config.gamePresenceEnabled);
|
||||||
|
if (config.gamePresenceEnabled) {
|
||||||
|
row.setAttribute("data-active", "true");
|
||||||
|
} else {
|
||||||
|
row.setAttribute("data-active", "false");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
row.addEventListener("click", (e) => {
|
||||||
|
if (settingsBtn.contains(e.target)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const config = getConfig();
|
||||||
|
config.gamePresenceEnabled = !config.gamePresenceEnabled;
|
||||||
|
setConfig(config);
|
||||||
|
applyState();
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsBtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
panel.style.display = panel.style.display === "flex" ? "none" : "flex";
|
||||||
|
});
|
||||||
|
|
||||||
|
applyState();
|
||||||
|
wrapper.appendChild(row);
|
||||||
|
wrapper.appendChild(panel);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectButton() {
|
||||||
|
const base = Array.from(document.querySelectorAll("a")).find((e) => {
|
||||||
|
const t = e.querySelector("div.d_flex.flex-g_1 > div");
|
||||||
|
return t && t.textContent.trim() === "Discord RPC";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!base) return;
|
||||||
|
if (document.querySelector(`[${CLONE_ATTR}]`)) return;
|
||||||
|
|
||||||
|
const newButton = createButton(base);
|
||||||
|
base.parentNode.appendChild(newButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
injectButton();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => injectButton());
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
244
avia_core/headliner.js
Normal file
244
avia_core/headliner.js
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
(function () {
|
||||||
|
if (window.__headliner) return;
|
||||||
|
window.__headliner = true;
|
||||||
|
|
||||||
|
window.__headlinerActive = localStorage.getItem("headlinerActive") === "true";
|
||||||
|
|
||||||
|
const TARGET_TEXT = "Spellchecker";
|
||||||
|
const CLONE_KEY = "data-headliner-cloned";
|
||||||
|
const STYLE_ID = "headliner-style";
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
content: "Sanctum",
|
||||||
|
left: "32",
|
||||||
|
top: "56",
|
||||||
|
fontSize: "15",
|
||||||
|
fontWeight: "700"
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
try {
|
||||||
|
let s = JSON.parse(localStorage.getItem("headlinerSettings"));
|
||||||
|
if (s && /^Sanctum V 1\.0\.[0-9]+$/.test(s.content)) {
|
||||||
|
s.content = defaults.content;
|
||||||
|
saveSettings(s);
|
||||||
|
}
|
||||||
|
return s || { ...defaults };
|
||||||
|
} catch {
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(settings) {
|
||||||
|
localStorage.setItem("headlinerSettings", JSON.stringify(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCSS(s) {
|
||||||
|
return `
|
||||||
|
svg path[d^="M466.17 254c-12.65"],
|
||||||
|
svg path[d^="M734.245 254c-15.377"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.flex-sh_0.h_29px.us_none.d_flex.ai_center.fill_var\\(--md-sys-color-on-surface\\).c_var\\(--md-sys-color-outline\\).bg_var\\(--md-sys-color-surface-container-high\\) {
|
||||||
|
position: relative !important;
|
||||||
|
color: transparent !important;
|
||||||
|
}
|
||||||
|
.flex-sh_0.h_29px.us_none.d_flex.ai_center.fill_var\\(--md-sys-color-on-surface\\).c_var\\(--md-sys-color-outline\\).bg_var\\(--md-sys-color-surface-container-high\\)::before {
|
||||||
|
content: "${s.content}";
|
||||||
|
position: absolute;
|
||||||
|
left: ${s.left}px;
|
||||||
|
top: ${s.top}%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: ${s.fontSize}px;
|
||||||
|
font-weight: ${s.fontWeight};
|
||||||
|
color: var(--md-sys-color-on-surface) !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCSS() {
|
||||||
|
const settings = loadSettings();
|
||||||
|
let style = document.getElementById(STYLE_ID);
|
||||||
|
if (!style) {
|
||||||
|
style = document.createElement("style");
|
||||||
|
style.id = STYLE_ID;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
style.textContent = buildCSS(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCSS() {
|
||||||
|
const style = document.getElementById(STYLE_ID);
|
||||||
|
if (style) style.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.__headlinerActive) applyCSS();
|
||||||
|
|
||||||
|
function applyActiveStyle(clone) {
|
||||||
|
const desc = clone.querySelector("div.d_flex.flex-g_1.flex-d_column > span");
|
||||||
|
const checkbox = clone.querySelector("mdui-checkbox");
|
||||||
|
|
||||||
|
if (window.__headlinerActive) {
|
||||||
|
clone.setAttribute("data-active", "true");
|
||||||
|
if (desc) desc.textContent = "Headliner is ON";
|
||||||
|
if (checkbox) checkbox.setAttribute("checked", "");
|
||||||
|
applyCSS();
|
||||||
|
} else {
|
||||||
|
clone.setAttribute("data-active", "false");
|
||||||
|
if (desc) desc.textContent = "Modify the Sanctum name in the titlebar to say anything you want";
|
||||||
|
if (checkbox) checkbox.removeAttribute("checked");
|
||||||
|
removeCSS();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPanel() {
|
||||||
|
const s = loadSettings();
|
||||||
|
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.id = "headliner-panel";
|
||||||
|
panel.style.cssText = `
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--md-sys-color-surface-container-highest);
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ label: "Content", key: "content", type: "text", value: s.content },
|
||||||
|
{ label: "Left (px)", key: "left", type: "number", value: s.left },
|
||||||
|
{ label: "Top (%)", key: "top", type: "number", value: s.top },
|
||||||
|
{ label: "Font Size", key: "fontSize", type: "number", value: s.fontSize },
|
||||||
|
{ label: "Font Weight", key: "fontWeight", type: "number", value: s.fontWeight }
|
||||||
|
];
|
||||||
|
|
||||||
|
fields.forEach(({ label, key, type, value }) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "display:flex; align-items:center; justify-content:space-between; gap:8px;";
|
||||||
|
|
||||||
|
const lbl = document.createElement("label");
|
||||||
|
lbl.textContent = label;
|
||||||
|
lbl.style.cssText = "flex-shrink:0; font-size:11px; opacity:0.8;";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = type;
|
||||||
|
input.value = value;
|
||||||
|
input.dataset.key = key;
|
||||||
|
input.style.cssText = `
|
||||||
|
width: ${type === "text" ? "160px" : "60px"};
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant);
|
||||||
|
background: var(--md-sys-color-surface-container);
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
font-size: 11px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
row.appendChild(lbl);
|
||||||
|
row.appendChild(input);
|
||||||
|
panel.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveBtn = document.createElement("button");
|
||||||
|
saveBtn.textContent = "Apply";
|
||||||
|
saveBtn.style.cssText = `
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const newSettings = { ...defaults };
|
||||||
|
panel.querySelectorAll("input").forEach(input => {
|
||||||
|
newSettings[input.dataset.key] = input.value;
|
||||||
|
});
|
||||||
|
saveSettings(newSettings);
|
||||||
|
if (window.__headlinerActive) applyCSS();
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.appendChild(saveBtn);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryInject() {
|
||||||
|
document.querySelectorAll("a.pos_relative").forEach(btn => {
|
||||||
|
if (
|
||||||
|
btn.hasAttribute(CLONE_KEY) ||
|
||||||
|
btn.hasAttribute("data-headliner-entry") ||
|
||||||
|
!btn.innerText.includes(TARGET_TEXT)
|
||||||
|
) return;
|
||||||
|
|
||||||
|
btn.setAttribute(CLONE_KEY, "true");
|
||||||
|
|
||||||
|
const clone = btn.cloneNode(true);
|
||||||
|
clone.removeAttribute(CLONE_KEY);
|
||||||
|
clone.setAttribute("data-headliner-entry", "true");
|
||||||
|
clone.setAttribute("data-active", "false");
|
||||||
|
|
||||||
|
const title = clone.querySelector("div.d_flex.flex-g_1.flex-d_column > div");
|
||||||
|
if (title) title.textContent = "Activate headliner";
|
||||||
|
|
||||||
|
const settingsBtn = document.createElement("div");
|
||||||
|
settingsBtn.title = "Edit headliner settings";
|
||||||
|
settingsBtn.style.cssText = "cursor: pointer; z-index: 10; flex-shrink: 0;";
|
||||||
|
settingsBtn.innerHTML = `
|
||||||
|
<div class="fill_var(--md-sys-color-on-surface) bg_var(--md-sys-color-surface-dim) w_36px h_36px d_flex flex-sh_0 ai_center jc_center bdr_var(--borderRadius-full)">
|
||||||
|
<span aria-hidden="true" class="material-symbols-outlined fs_inherit fw_undefined!" style="display: block; font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0;">settings</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existingIcon = clone.querySelector("div.fill_var\\(--md-sys-color-on-surface\\)");
|
||||||
|
if (existingIcon) {
|
||||||
|
existingIcon.replaceWith(settingsBtn);
|
||||||
|
} else {
|
||||||
|
clone.prepend(settingsBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.style.cssText = "display: flex; flex-direction: column;";
|
||||||
|
|
||||||
|
const panel = buildPanel();
|
||||||
|
|
||||||
|
settingsBtn.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
panel.style.display = panel.style.display === "flex" ? "none" : "flex";
|
||||||
|
});
|
||||||
|
|
||||||
|
clone.addEventListener("click", e => {
|
||||||
|
if (settingsBtn.contains(e.target)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.__headlinerActive = !window.__headlinerActive;
|
||||||
|
localStorage.setItem("headlinerActive", window.__headlinerActive);
|
||||||
|
applyActiveStyle(clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
applyActiveStyle(clone);
|
||||||
|
|
||||||
|
wrapper.appendChild(clone);
|
||||||
|
wrapper.appendChild(panel);
|
||||||
|
|
||||||
|
btn.parentNode.insertBefore(wrapper, btn.nextSibling);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryInject();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => tryInject());
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
|
|
@ -6,7 +6,22 @@
|
||||||
const LINKTREE_URL = "https://linktr.ee/GermanAvaLilac";
|
const LINKTREE_URL = "https://linktr.ee/GermanAvaLilac";
|
||||||
const STOAT_SERVER_URL = "https://stt.gg/GvBhcejB";
|
const STOAT_SERVER_URL = "https://stt.gg/GvBhcejB";
|
||||||
|
|
||||||
function toggleQuickCSSPanel() {
|
function preloadMonaco() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (window.monaco) return resolve();
|
||||||
|
const loader = document.createElement("script");
|
||||||
|
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
|
||||||
|
loader.onload = function () {
|
||||||
|
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
|
||||||
|
require(["vs/editor/editor.main"], () => resolve());
|
||||||
|
};
|
||||||
|
document.head.appendChild(loader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleQuickCSSPanel() {
|
||||||
|
await preloadMonaco();
|
||||||
|
|
||||||
let panel = document.getElementById('avia-quickcss-panel');
|
let panel = document.getElementById('avia-quickcss-panel');
|
||||||
if (panel) {
|
if (panel) {
|
||||||
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
|
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
|
||||||
|
|
@ -15,80 +30,88 @@
|
||||||
|
|
||||||
panel = document.createElement('div');
|
panel = document.createElement('div');
|
||||||
panel.id = 'avia-quickcss-panel';
|
panel.id = 'avia-quickcss-panel';
|
||||||
panel.style.position = 'fixed';
|
Object.assign(panel.style, {
|
||||||
panel.style.bottom = '24px';
|
position: 'fixed',
|
||||||
panel.style.right = '24px';
|
bottom: '24px',
|
||||||
panel.style.width = '420px';
|
right: '24px',
|
||||||
panel.style.height = '340px';
|
width: '650px',
|
||||||
panel.style.background = 'var(--md-sys-color-surface, #1e1e1e)';
|
height: '420px',
|
||||||
panel.style.color = 'var(--md-sys-color-on-surface, #fff)';
|
background: 'var(--md-sys-color-surface, #1e1e1e)',
|
||||||
panel.style.borderRadius = '16px';
|
color: 'var(--md-sys-color-on-surface, #fff)',
|
||||||
panel.style.boxShadow = '0 8px 28px rgba(0,0,0,0.35)';
|
borderRadius: '16px',
|
||||||
panel.style.zIndex = '999999';
|
boxShadow: '0 8px 28px rgba(0,0,0,0.35)',
|
||||||
panel.style.display = 'flex';
|
zIndex: '999999',
|
||||||
panel.style.flexDirection = 'column';
|
display: 'flex',
|
||||||
panel.style.overflow = 'hidden';
|
flexDirection: 'column',
|
||||||
panel.style.border = '1px solid rgba(255,255,255,0.08)';
|
overflow: 'hidden',
|
||||||
panel.style.backdropFilter = 'blur(12px)';
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
backdropFilter: 'blur(12px)'
|
||||||
|
});
|
||||||
|
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.textContent = 'QuickCSS';
|
header.textContent = 'QuickCSS';
|
||||||
header.style.padding = '14px 16px';
|
Object.assign(header.style, {
|
||||||
header.style.fontWeight = '600';
|
padding: '14px 16px',
|
||||||
header.style.fontSize = '14px';
|
fontWeight: '600',
|
||||||
header.style.letterSpacing = '0.3px';
|
fontSize: '14px',
|
||||||
header.style.background = 'var(--md-sys-color-surface-container, rgba(255,255,255,0.04))';
|
letterSpacing: '0.3px',
|
||||||
header.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
|
background: 'var(--md-sys-color-surface-container, rgba(255,255,255,0.04))',
|
||||||
header.style.cursor = 'move';
|
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
cursor: 'move',
|
||||||
|
color: '#fff'
|
||||||
|
});
|
||||||
|
|
||||||
const closeBtn = document.createElement('div');
|
const closeBtn = document.createElement('div');
|
||||||
closeBtn.textContent = '✕';
|
closeBtn.textContent = '✕';
|
||||||
closeBtn.style.position = 'absolute';
|
Object.assign(closeBtn.style, {
|
||||||
closeBtn.style.top = '12px';
|
position: 'absolute',
|
||||||
closeBtn.style.right = '16px';
|
top: '12px',
|
||||||
closeBtn.style.cursor = 'pointer';
|
right: '16px',
|
||||||
closeBtn.style.opacity = '0.7';
|
cursor: 'pointer',
|
||||||
|
opacity: '0.7',
|
||||||
|
color: '#fff'
|
||||||
|
});
|
||||||
closeBtn.onmouseenter = () => closeBtn.style.opacity = '1';
|
closeBtn.onmouseenter = () => closeBtn.style.opacity = '1';
|
||||||
closeBtn.onmouseleave = () => closeBtn.style.opacity = '0.7';
|
closeBtn.onmouseleave = () => closeBtn.style.opacity = '0.7';
|
||||||
closeBtn.onclick = () => panel.style.display = 'none';
|
closeBtn.onclick = () => panel.style.display = 'none';
|
||||||
|
|
||||||
const textarea = document.createElement('textarea');
|
const editorContainer = document.createElement('div');
|
||||||
textarea.style.flex = '1';
|
editorContainer.style.flex = '1';
|
||||||
textarea.style.border = 'none';
|
|
||||||
textarea.style.outline = 'none';
|
|
||||||
textarea.style.resize = 'none';
|
|
||||||
textarea.style.padding = '16px';
|
|
||||||
textarea.style.background = 'transparent';
|
|
||||||
textarea.style.color = 'inherit';
|
|
||||||
textarea.style.fontFamily = 'monospace';
|
|
||||||
textarea.style.fontSize = '13px';
|
|
||||||
textarea.style.lineHeight = '1.4';
|
|
||||||
textarea.value = localStorage.getItem('avia_quickcss') || '';
|
|
||||||
|
|
||||||
textarea.addEventListener('input', () => {
|
|
||||||
localStorage.setItem('avia_quickcss', textarea.value);
|
|
||||||
applyQuickCSS(textarea.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
panel.appendChild(header);
|
panel.appendChild(header);
|
||||||
panel.appendChild(closeBtn);
|
panel.appendChild(closeBtn);
|
||||||
panel.appendChild(textarea);
|
panel.appendChild(editorContainer);
|
||||||
document.body.appendChild(panel);
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
const editor = monaco.editor.create(editorContainer, {
|
||||||
|
value: localStorage.getItem('avia_quickcss') || '',
|
||||||
|
language: 'css',
|
||||||
|
theme: 'vs-dark',
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: 'on'
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.onDidChangeModelContent(() => {
|
||||||
|
const value = editor.getValue();
|
||||||
|
localStorage.setItem('avia_quickcss', value);
|
||||||
|
applyQuickCSS(value);
|
||||||
|
});
|
||||||
|
|
||||||
let isDragging = false, offsetX, offsetY;
|
let isDragging = false, offsetX, offsetY;
|
||||||
header.addEventListener('mousedown', (e) => {
|
header.addEventListener('mousedown', e => {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
offsetX = e.clientX - panel.offsetLeft;
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
offsetY = e.clientY - panel.offsetTop;
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = '';
|
||||||
});
|
});
|
||||||
|
document.addEventListener('mousemove', e => {
|
||||||
document.addEventListener('mousemove', (e) => {
|
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
panel.style.left = (e.clientX - offsetX) + 'px';
|
panel.style.left = (e.clientX - offsetX) + 'px';
|
||||||
panel.style.top = (e.clientY - offsetY) + 'px';
|
panel.style.top = (e.clientY - offsetY) + 'px';
|
||||||
|
|
@ -204,8 +227,8 @@
|
||||||
styleTag.id = 'custom-font-style';
|
styleTag.id = 'custom-font-style';
|
||||||
document.head.appendChild(styleTag);
|
document.head.appendChild(styleTag);
|
||||||
}
|
}
|
||||||
let ext = url.split('.').pop().toLowerCase();
|
const ext = url.split('.').pop().toLowerCase();
|
||||||
let formatMap = {
|
const formatMap = {
|
||||||
ttf: 'truetype',
|
ttf: 'truetype',
|
||||||
otf: 'opentype',
|
otf: 'opentype',
|
||||||
woff: 'woff',
|
woff: 'woff',
|
||||||
|
|
@ -213,7 +236,7 @@
|
||||||
eot: 'embedded-opentype',
|
eot: 'embedded-opentype',
|
||||||
css: 'truetype'
|
css: 'truetype'
|
||||||
};
|
};
|
||||||
let format = formatMap[ext] || '';
|
const format = formatMap[ext] || '';
|
||||||
styleTag.textContent = `
|
styleTag.textContent = `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: '${fontName}';
|
font-family: '${fontName}';
|
||||||
|
|
@ -243,21 +266,21 @@
|
||||||
const appearanceBtn = Array.from(document.querySelectorAll('a')).find(a => a.textContent.trim() === 'Appearance');
|
const appearanceBtn = Array.from(document.querySelectorAll('a')).find(a => a.textContent.trim() === 'Appearance');
|
||||||
if (!appearanceBtn) return;
|
if (!appearanceBtn) return;
|
||||||
|
|
||||||
const aviaHeader = [...document.querySelectorAll('span')]
|
const aviaHeader = [...document.querySelectorAll('span')]
|
||||||
.find(s => s.textContent.trim() === "AVIA CLIENT SETTINGS");
|
.find(s => s.textContent.trim() === "AVIA CLIENT SETTINGS");
|
||||||
if (!aviaHeader) return;
|
if (!aviaHeader) return;
|
||||||
|
|
||||||
const aviaContainer = aviaHeader.closest('.d_flex.flex-d_column');
|
const aviaContainer = aviaHeader.closest('.d_flex.flex-d_column');
|
||||||
if (!aviaContainer) return;
|
if (!aviaContainer) return;
|
||||||
|
|
||||||
const targetParent = aviaContainer.querySelector('.d_flex.flex-d_column.gap_var\\(--gap-s\\)');
|
const targetParent = aviaContainer.querySelector('.d_flex.flex-d_column.gap_var\\(--gap-s\\)');
|
||||||
if (!targetParent) return;
|
if (!targetParent) return;
|
||||||
|
|
||||||
if (!document.getElementById('stoat-fake-linktree')) {
|
if (!document.getElementById('stoat-fake-linktree')) {
|
||||||
const linktreeBtn = appearanceBtn.cloneNode(true);
|
const linktreeBtn = appearanceBtn.cloneNode(true);
|
||||||
linktreeBtn.id = 'stoat-fake-linktree';
|
linktreeBtn.id = 'stoat-fake-linktree';
|
||||||
const textNode = Array.from(linktreeBtn.querySelectorAll('div')).find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
const textNode = Array.from(linktreeBtn.querySelectorAll('div')).find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
||||||
if (textNode) textNode.textContent = "(Avia) Ava's Linktree";
|
if (textNode) textNode.textContent = "(Sanctum) Ava's Linktree";
|
||||||
setIcon(linktreeBtn, "monitor");
|
setIcon(linktreeBtn, "monitor");
|
||||||
linktreeBtn.addEventListener('click', () => window.open(LINKTREE_URL, "_blank"));
|
linktreeBtn.addEventListener('click', () => window.open(LINKTREE_URL, "_blank"));
|
||||||
targetParent.appendChild(linktreeBtn);
|
targetParent.appendChild(linktreeBtn);
|
||||||
|
|
@ -265,7 +288,7 @@ if (!targetParent) return;
|
||||||
const stoatBtn = appearanceBtn.cloneNode(true);
|
const stoatBtn = appearanceBtn.cloneNode(true);
|
||||||
stoatBtn.id = 'stoat-fake-stoatserver';
|
stoatBtn.id = 'stoat-fake-stoatserver';
|
||||||
const stoatTextNode = Array.from(stoatBtn.querySelectorAll('div')).find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
const stoatTextNode = Array.from(stoatBtn.querySelectorAll('div')).find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
||||||
if (stoatTextNode) stoatTextNode.textContent = "(Avia) Stoat Server";
|
if (stoatTextNode) stoatTextNode.textContent = "(Sanctum) Stoat Server";
|
||||||
setIcon(stoatBtn, "monitor");
|
setIcon(stoatBtn, "monitor");
|
||||||
stoatBtn.addEventListener('click', () => window.open(STOAT_SERVER_URL, "_blank"));
|
stoatBtn.addEventListener('click', () => window.open(STOAT_SERVER_URL, "_blank"));
|
||||||
targetParent.appendChild(stoatBtn);
|
targetParent.appendChild(stoatBtn);
|
||||||
|
|
@ -275,18 +298,16 @@ if (!targetParent) return;
|
||||||
const newBtn = appearanceBtn.cloneNode(true);
|
const newBtn = appearanceBtn.cloneNode(true);
|
||||||
newBtn.id = 'stoat-fake-loadfont';
|
newBtn.id = 'stoat-fake-loadfont';
|
||||||
const textNode = Array.from(newBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
const textNode = Array.from(newBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
||||||
if (textNode) textNode.textContent = "(Avia) Font Loader";
|
if (textNode) textNode.textContent = "(Sanctum) Font Loader";
|
||||||
setIcon(newBtn, "upload");
|
setIcon(newBtn, "upload");
|
||||||
newBtn.addEventListener('click', showFontLoaderPopup);
|
newBtn.addEventListener('click', showFontLoaderPopup);
|
||||||
|
|
||||||
const stoatBtn = document.getElementById('stoat-fake-stoatserver');
|
|
||||||
targetParent.appendChild(newBtn);
|
targetParent.appendChild(newBtn);
|
||||||
|
|
||||||
if (!document.getElementById('stoat-fake-removefont')) {
|
if (!document.getElementById('stoat-fake-removefont')) {
|
||||||
const removeBtn = appearanceBtn.cloneNode(true);
|
const removeBtn = appearanceBtn.cloneNode(true);
|
||||||
removeBtn.id = 'stoat-fake-removefont';
|
removeBtn.id = 'stoat-fake-removefont';
|
||||||
const removeTextNode = Array.from(removeBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
const removeTextNode = Array.from(removeBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
||||||
if (removeTextNode) removeTextNode.textContent = "(Avia) Remove selected font";
|
if (removeTextNode) removeTextNode.textContent = "(Sanctum) Remove selected font";
|
||||||
setIcon(removeBtn, "refresh");
|
setIcon(removeBtn, "refresh");
|
||||||
removeBtn.addEventListener('click', showRemoveFontPopup);
|
removeBtn.addEventListener('click', showRemoveFontPopup);
|
||||||
targetParent.appendChild(removeBtn);
|
targetParent.appendChild(removeBtn);
|
||||||
|
|
@ -297,15 +318,11 @@ if (!targetParent) return;
|
||||||
const quickCssBtn = appearanceBtn.cloneNode(true);
|
const quickCssBtn = appearanceBtn.cloneNode(true);
|
||||||
quickCssBtn.id = 'stoat-fake-quickcss';
|
quickCssBtn.id = 'stoat-fake-quickcss';
|
||||||
const quickCssTextNode = Array.from(quickCssBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
const quickCssTextNode = Array.from(quickCssBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
||||||
if (quickCssTextNode) quickCssTextNode.textContent = "(Avia) QuickCSS";
|
if (quickCssTextNode) quickCssTextNode.textContent = "(Sanctum) QuickCSS";
|
||||||
setIcon(quickCssBtn, "code");
|
setIcon(quickCssBtn, "code");
|
||||||
quickCssBtn.addEventListener('click', toggleQuickCSSPanel);
|
quickCssBtn.addEventListener('click', toggleQuickCSSPanel);
|
||||||
|
targetParent.appendChild(quickCssBtn);
|
||||||
const lastBtn = document.getElementById('stoat-fake-removefont') ||
|
}
|
||||||
document.getElementById('stoat-fake-loadfont') ||
|
|
||||||
document.getElementById('stoat-fake-stoatserver') ||
|
|
||||||
document.getElementById('stoat-fake-linktree');
|
|
||||||
targetParent.appendChild(quickCssBtn); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyQuickCSS(css) {
|
function applyQuickCSS(css) {
|
||||||
|
|
@ -339,4 +356,6 @@ targetParent.appendChild(quickCssBtn); }
|
||||||
injectButtons();
|
injectButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
preloadMonaco();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
575
avia_core/pluginsupport.js
Normal file
575
avia_core/pluginsupport.js
Normal file
|
|
@ -0,0 +1,575 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
if (window.__AVIA_PLUGINS_LOADED__) return;
|
||||||
|
window.__AVIA_PLUGINS_LOADED__ = true;
|
||||||
|
|
||||||
|
const STORAGE_KEY = "avia_plugins";
|
||||||
|
|
||||||
|
const runningPlugins = {};
|
||||||
|
const pluginErrors = {};
|
||||||
|
const injectionQueue = [];
|
||||||
|
|
||||||
|
const getPlugins = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
|
const setPlugins = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
|
||||||
|
function normalizePluginUrl(url) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
|
||||||
|
if (u.hostname === "github.com") {
|
||||||
|
|
||||||
|
const m = u.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
||||||
|
if (m) {
|
||||||
|
return `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3]}/${m[4]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (u.hostname === "raw.githubusercontent.com") return url;
|
||||||
|
|
||||||
|
|
||||||
|
if (u.hostname === "raw.codeberg.page") return url;
|
||||||
|
|
||||||
|
if (u.hostname === "codeberg.org") {
|
||||||
|
const parts = u.pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
if (parts.length >= 5 && (parts[2] === "raw" || parts[2] === "src")) {
|
||||||
|
const user = parts[0];
|
||||||
|
const repo = parts[1];
|
||||||
|
|
||||||
|
const branchName = parts[3] === "branch" || parts[3] === "commit" || parts[3] === "tag"
|
||||||
|
? parts[4]
|
||||||
|
: parts[3];
|
||||||
|
const fileStart = parts[3] === "branch" || parts[3] === "commit" || parts[3] === "tag"
|
||||||
|
? 5
|
||||||
|
: 4;
|
||||||
|
const filePath = parts.slice(fileStart).join("/");
|
||||||
|
return `https://raw.codeberg.page/${user}/${repo}/@${branchName}/${filePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length >= 4 && parts[2] === "raw") {
|
||||||
|
const user = parts[0];
|
||||||
|
const repo = parts[1];
|
||||||
|
const branchName = parts[3];
|
||||||
|
const filePath = parts.slice(4).join("/");
|
||||||
|
return `https://raw.codeberg.page/${user}/${repo}/@${branchName}/${filePath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processQueue() {
|
||||||
|
if (processQueue.running) return;
|
||||||
|
processQueue.running = true;
|
||||||
|
while (injectionQueue.length) {
|
||||||
|
const { plugin, force } = injectionQueue.shift();
|
||||||
|
await loadPluginInternal(plugin, force);
|
||||||
|
}
|
||||||
|
processQueue.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queuePlugin(plugin, force = false) {
|
||||||
|
injectionQueue.push({ plugin, force });
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPluginInternal(plugin, force = false) {
|
||||||
|
if (runningPlugins[plugin.url] && !force) return;
|
||||||
|
if (force) stopPlugin(plugin);
|
||||||
|
try {
|
||||||
|
const fetchUrl = normalizePluginUrl(plugin.url);
|
||||||
|
const res = await fetch(fetchUrl);
|
||||||
|
if (!res.ok) throw new Error("Fetch failed");
|
||||||
|
const code = await res.text();
|
||||||
|
delete pluginErrors[plugin.url];
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.textContent = code;
|
||||||
|
script.dataset.pluginUrl = plugin.url;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
runningPlugins[plugin.url] = script;
|
||||||
|
} catch {
|
||||||
|
pluginErrors[plugin.url] = true;
|
||||||
|
}
|
||||||
|
renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPlugin(plugin) {
|
||||||
|
const script = runningPlugins[plugin.url];
|
||||||
|
if (!script) return;
|
||||||
|
script.remove();
|
||||||
|
delete runningPlugins[plugin.url];
|
||||||
|
delete pluginErrors[plugin.url];
|
||||||
|
renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadMonaco() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (window.monaco) return resolve();
|
||||||
|
const loader = document.createElement("script");
|
||||||
|
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
|
||||||
|
loader.onload = function () {
|
||||||
|
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
|
||||||
|
require(["vs/editor/editor.main"], () => resolve());
|
||||||
|
};
|
||||||
|
document.head.appendChild(loader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openViewerPanel(plugin) {
|
||||||
|
await preloadMonaco();
|
||||||
|
|
||||||
|
const existing = document.getElementById("avia-plugin-viewer-panel");
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.id = "avia-plugin-viewer-panel";
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "24px",
|
||||||
|
left: "24px",
|
||||||
|
width: "700px",
|
||||||
|
height: "480px",
|
||||||
|
background: "var(--md-sys-color-surface, #1e1e1e)",
|
||||||
|
borderRadius: "16px",
|
||||||
|
boxShadow: "0 8px 28px rgba(0,0,0,0.45)",
|
||||||
|
zIndex: "9999999",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
color: "#fff"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "14px 16px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "14px",
|
||||||
|
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleText = document.createElement("span");
|
||||||
|
titleText.textContent = `Viewing: ${plugin.name}`;
|
||||||
|
titleText.style.flex = "1";
|
||||||
|
|
||||||
|
const readOnlyBadge = document.createElement("span");
|
||||||
|
readOnlyBadge.textContent = "READ ONLY";
|
||||||
|
Object.assign(readOnlyBadge.style, {
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: "700",
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
padding: "2px 8px",
|
||||||
|
borderRadius: "20px",
|
||||||
|
background: "rgba(255,180,0,0.15)",
|
||||||
|
color: "#ffb400",
|
||||||
|
border: "1px solid rgba(255,180,0,0.3)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("div");
|
||||||
|
closeBtn.textContent = "✕";
|
||||||
|
Object.assign(closeBtn.style, {
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: "0.6",
|
||||||
|
fontSize: "15px",
|
||||||
|
lineHeight: "1",
|
||||||
|
padding: "2px 4px"
|
||||||
|
});
|
||||||
|
closeBtn.onmouseenter = () => closeBtn.style.opacity = "1";
|
||||||
|
closeBtn.onmouseleave = () => closeBtn.style.opacity = "0.6";
|
||||||
|
closeBtn.onclick = () => panel.remove();
|
||||||
|
|
||||||
|
header.appendChild(titleText);
|
||||||
|
header.appendChild(readOnlyBadge);
|
||||||
|
header.appendChild(closeBtn);
|
||||||
|
|
||||||
|
const urlBar = document.createElement("div");
|
||||||
|
Object.assign(urlBar.style, {
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
||||||
|
fontSize: "11px",
|
||||||
|
color: "rgba(255,255,255,0.35)",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
background: "rgba(0,0,0,0.15)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
urlBar.textContent = plugin.url;
|
||||||
|
urlBar.title = plugin.url;
|
||||||
|
|
||||||
|
const editorContainer = document.createElement("div");
|
||||||
|
editorContainer.style.flex = "1";
|
||||||
|
editorContainer.style.overflow = "hidden";
|
||||||
|
|
||||||
|
const loadingMsg = document.createElement("div");
|
||||||
|
Object.assign(loadingMsg.style, {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "100%",
|
||||||
|
opacity: "0.4",
|
||||||
|
fontSize: "13px"
|
||||||
|
});
|
||||||
|
loadingMsg.textContent = "Fetching source…";
|
||||||
|
editorContainer.appendChild(loadingMsg);
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(urlBar);
|
||||||
|
panel.appendChild(editorContainer);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
enableDragOn(panel, header);
|
||||||
|
|
||||||
|
let code;
|
||||||
|
try {
|
||||||
|
const fetchUrl = normalizePluginUrl(plugin.url);
|
||||||
|
const res = await fetch(fetchUrl);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
code = await res.text();
|
||||||
|
} catch (err) {
|
||||||
|
loadingMsg.textContent = `Failed to fetch source: ${err.message}`;
|
||||||
|
loadingMsg.style.color = "#ff4d4d";
|
||||||
|
loadingMsg.style.opacity = "1";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorContainer.removeChild(loadingMsg);
|
||||||
|
|
||||||
|
monaco.editor.create(editorContainer, {
|
||||||
|
value: code,
|
||||||
|
language: "javascript",
|
||||||
|
theme: "vs-dark",
|
||||||
|
readOnly: true,
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: true },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: "off",
|
||||||
|
domReadOnly: true,
|
||||||
|
renderValidationDecorations: "off",
|
||||||
|
renderLineHighlight: "none",
|
||||||
|
cursorStyle: "block",
|
||||||
|
cursorBlinking: "solid"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePluginsPanel() {
|
||||||
|
let panel = document.getElementById('avia-plugins-panel');
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panel = document.createElement('div');
|
||||||
|
panel.id = 'avia-plugins-panel';
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '24px',
|
||||||
|
right: '24px',
|
||||||
|
width: '520px',
|
||||||
|
height: '460px',
|
||||||
|
background: 'var(--md-sys-color-surface, #1e1e1e)',
|
||||||
|
color: 'var(--md-sys-color-on-surface, #fff)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 8px 28px rgba(0,0,0,0.35)',
|
||||||
|
zIndex: '999999',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
backdropFilter: 'blur(12px)'
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.textContent = 'Plugins';
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: '14px 16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '14px',
|
||||||
|
background: 'var(--md-sys-color-surface-container, rgba(255,255,255,0.04))',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
cursor: 'move'
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('div');
|
||||||
|
closeBtn.textContent = '✕';
|
||||||
|
Object.assign(closeBtn.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '12px',
|
||||||
|
right: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: '0.7'
|
||||||
|
});
|
||||||
|
closeBtn.onclick = () => panel.style.display = 'none';
|
||||||
|
|
||||||
|
const controlsBar = document.createElement('div');
|
||||||
|
Object.assign(controlsBar.style, {
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
flex: '0 0 auto'
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.id = 'avia-plugins-content';
|
||||||
|
Object.assign(content.style, {
|
||||||
|
flex: '1',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '16px'
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameInput = document.createElement('input');
|
||||||
|
nameInput.placeholder = 'Name';
|
||||||
|
styleInput(nameInput);
|
||||||
|
nameInput.style.width = '110px';
|
||||||
|
|
||||||
|
const urlInput = document.createElement('input');
|
||||||
|
urlInput.placeholder = 'Plugin URL';
|
||||||
|
styleInput(urlInput);
|
||||||
|
urlInput.style.flex = '1';
|
||||||
|
|
||||||
|
const addBtn = document.createElement('button');
|
||||||
|
addBtn.textContent = '+ Add';
|
||||||
|
styleBtn(addBtn);
|
||||||
|
addBtn.onclick = () => {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
if (!name || !url) return;
|
||||||
|
const plugins = getPlugins();
|
||||||
|
plugins.push({ name, url, enabled: false });
|
||||||
|
setPlugins(plugins);
|
||||||
|
nameInput.value = '';
|
||||||
|
urlInput.value = '';
|
||||||
|
renderPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshAll = document.createElement('button');
|
||||||
|
refreshAll.textContent = 'Refresh';
|
||||||
|
styleBtn(refreshAll);
|
||||||
|
refreshAll.onclick = () => {
|
||||||
|
const plugins = getPlugins();
|
||||||
|
plugins.forEach(p => {
|
||||||
|
if (p.enabled) queuePlugin(p, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
controlsBar.appendChild(nameInput);
|
||||||
|
controlsBar.appendChild(urlInput);
|
||||||
|
controlsBar.appendChild(addBtn);
|
||||||
|
controlsBar.appendChild(refreshAll);
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(closeBtn);
|
||||||
|
panel.appendChild(controlsBar);
|
||||||
|
panel.appendChild(content);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
enableDragOn(panel, header);
|
||||||
|
renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPanel() {
|
||||||
|
const content = document.getElementById('avia-plugins-content');
|
||||||
|
if (!content) return;
|
||||||
|
content.innerHTML = '';
|
||||||
|
const plugins = getPlugins();
|
||||||
|
const runningSnapshot = { ...runningPlugins };
|
||||||
|
const errorSnapshot = { ...pluginErrors };
|
||||||
|
|
||||||
|
if (plugins.length === 0) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.textContent = 'No plugins yet. Add one above.';
|
||||||
|
Object.assign(empty.style, { opacity: '0.4', fontSize: '13px' });
|
||||||
|
content.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.forEach((plugin, index) => {
|
||||||
|
const isRunning = !!runningSnapshot[plugin.url];
|
||||||
|
const hasError = !!errorSnapshot[plugin.url];
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
Object.assign(row.style, {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '12px',
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: 'rgba(255,255,255,0.04)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)'
|
||||||
|
});
|
||||||
|
|
||||||
|
const left = document.createElement('div');
|
||||||
|
Object.assign(left.style, { display: 'flex', alignItems: 'center', gap: '10px' });
|
||||||
|
|
||||||
|
const statusDot = document.createElement('div');
|
||||||
|
Object.assign(statusDot.style, {
|
||||||
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
flexShrink: '0'
|
||||||
|
});
|
||||||
|
if (hasError) {
|
||||||
|
statusDot.style.background = '#ff4d4d';
|
||||||
|
statusDot.style.boxShadow = '0 0 6px #ff4d4d';
|
||||||
|
} else if (isRunning) {
|
||||||
|
statusDot.style.background = '#4dff88';
|
||||||
|
statusDot.style.boxShadow = '0 0 6px #4dff88';
|
||||||
|
} else {
|
||||||
|
statusDot.style.background = '#777';
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.textContent = plugin.name;
|
||||||
|
name.style.fontSize = '13px';
|
||||||
|
|
||||||
|
left.appendChild(statusDot);
|
||||||
|
left.appendChild(name);
|
||||||
|
|
||||||
|
const controls = document.createElement('div');
|
||||||
|
Object.assign(controls.style, { display: 'flex', gap: '6px' });
|
||||||
|
|
||||||
|
const toggle = document.createElement('button');
|
||||||
|
toggle.textContent = plugin.enabled ? 'Disable' : 'Enable';
|
||||||
|
styleBtn(toggle);
|
||||||
|
toggle.onclick = () => {
|
||||||
|
plugin.enabled = !plugin.enabled;
|
||||||
|
setPlugins(plugins);
|
||||||
|
if (plugin.enabled) queuePlugin(plugin);
|
||||||
|
else stopPlugin(plugin);
|
||||||
|
renderPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewBtn = document.createElement('button');
|
||||||
|
viewBtn.textContent = 'View';
|
||||||
|
styleBtn(viewBtn, 'rgba(100,160,255,0.15)');
|
||||||
|
viewBtn.onclick = () => openViewerPanel(plugin);
|
||||||
|
|
||||||
|
const remove = document.createElement('button');
|
||||||
|
remove.textContent = '✕';
|
||||||
|
styleBtn(remove, 'rgba(255,80,80,0.15)');
|
||||||
|
remove.onclick = () => {
|
||||||
|
stopPlugin(plugin);
|
||||||
|
plugins.splice(index, 1);
|
||||||
|
setPlugins(plugins);
|
||||||
|
renderPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
controls.appendChild(toggle);
|
||||||
|
controls.appendChild(viewBtn);
|
||||||
|
controls.appendChild(remove);
|
||||||
|
row.appendChild(left);
|
||||||
|
row.appendChild(controls);
|
||||||
|
content.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleInput(input) {
|
||||||
|
Object.assign(input.style, {
|
||||||
|
padding: '6px 8px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '13px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleBtn(btn, bg) {
|
||||||
|
Object.assign(btn.style, {
|
||||||
|
padding: '5px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
background: bg || 'rgba(255,255,255,0.08)',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '12px',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
});
|
||||||
|
btn.onmouseenter = () => btn.style.opacity = '0.75';
|
||||||
|
btn.onmouseleave = () => btn.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableDragOn(panel, header) {
|
||||||
|
let isDragging = false, offsetX, offsetY;
|
||||||
|
header.addEventListener('mousedown', e => {
|
||||||
|
isDragging = true;
|
||||||
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
isDragging = false;
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
});
|
||||||
|
document.addEventListener('mousemove', e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
panel.style.left = (e.clientX - offsetX) + 'px';
|
||||||
|
panel.style.top = (e.clientY - offsetY) + 'px';
|
||||||
|
panel.style.right = 'auto';
|
||||||
|
panel.style.bottom = 'auto';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectButtons() {
|
||||||
|
if (document.getElementById('stoat-fake-plugins')) return;
|
||||||
|
const appearanceBtn = [...document.querySelectorAll('a')]
|
||||||
|
.find(a => a.textContent.trim() === 'Appearance');
|
||||||
|
if (!appearanceBtn) return;
|
||||||
|
const referenceNode = document.getElementById('stoat-fake-quickcss');
|
||||||
|
if (!referenceNode) return;
|
||||||
|
const pluginsBtn = appearanceBtn.cloneNode(true);
|
||||||
|
pluginsBtn.id = 'stoat-fake-plugins';
|
||||||
|
const textNode = [...pluginsBtn.querySelectorAll('div')]
|
||||||
|
.find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
||||||
|
if (textNode) textNode.textContent = "(Sanctum) Plugins";
|
||||||
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
|
const oldSvg = pluginsBtn.querySelector('svg');
|
||||||
|
if (oldSvg) oldSvg.remove();
|
||||||
|
const svg = document.createElementNS(svgNS, "svg");
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("width", "20");
|
||||||
|
svg.setAttribute("height", "20");
|
||||||
|
svg.setAttribute("fill", "currentColor");
|
||||||
|
svg.style.marginRight = "8px";
|
||||||
|
const path = document.createElementNS(svgNS, "path");
|
||||||
|
path.setAttribute("d", "M20.5 11H19V7a2 2 0 00-2-2h-4V3.5a2.5 2.5 0 00-5 0V5H4a2 2 0 00-2 2v3.8h1.5c1.5 0 2.7 1.2 2.7 2.7S5 16.2 3.5 16.2H2V20a2 2 0 002 2h3.8v-1.5c0-1.5 1.2-2.7 2.7-2.7s2.7 1.2 2.7 2.7V22H17a2 2 0 002-2v-4h1.5a2.5 2.5 0 000-5z");
|
||||||
|
svg.appendChild(path);
|
||||||
|
pluginsBtn.insertBefore(svg, pluginsBtn.firstChild);
|
||||||
|
pluginsBtn.addEventListener('click', togglePluginsPanel);
|
||||||
|
referenceNode.parentElement.insertBefore(pluginsBtn, referenceNode.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForBody(callback) {
|
||||||
|
if (document.body) callback();
|
||||||
|
else new MutationObserver((obs) => {
|
||||||
|
if (document.body) { obs.disconnect(); callback(); }
|
||||||
|
}).observe(document.documentElement, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForBody(() => {
|
||||||
|
const observer = new MutationObserver(() => injectButtons());
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
injectButtons();
|
||||||
|
preloadMonaco();
|
||||||
|
});
|
||||||
|
|
||||||
|
getPlugins().forEach(plugin => {
|
||||||
|
if (plugin.enabled) queuePlugin(plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
479
avia_core/repofrontend.js
Normal file
479
avia_core/repofrontend.js
Normal file
|
|
@ -0,0 +1,479 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
if (window.__AVIA_OFFICIAL_REPO_LOADED__) return;
|
||||||
|
window.__AVIA_OFFICIAL_REPO_LOADED__ = true;
|
||||||
|
|
||||||
|
const STORAGE_KEY = "avia_plugins";
|
||||||
|
const OFFICIAL_REPO_URL = "https://avalilac.github.io/PluginRepo/pluginrepobackend.js";
|
||||||
|
const THEMES_REGISTRY_URL = "https://avalilac.github.io/PluginRepo/themebackend/themerepobackend.js";
|
||||||
|
|
||||||
|
const getPlugins = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
|
const setPlugins = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
let repoContent;
|
||||||
|
let currentRepoData = [];
|
||||||
|
let currentThemeData = [];
|
||||||
|
let searchInput;
|
||||||
|
let activeTab = "plugins"; // "plugins" | "themes"
|
||||||
|
|
||||||
|
document.getElementById("avia-official-repo-btn")?.remove();
|
||||||
|
|
||||||
|
function triggerManagerRefresh() {
|
||||||
|
const panel = document.getElementById("avia-plugins-panel");
|
||||||
|
if (!panel) return;
|
||||||
|
const refreshBtn = Array.from(panel.querySelectorAll("button"))
|
||||||
|
.find(b => b.textContent.trim() === "Refresh");
|
||||||
|
if (refreshBtn) refreshBtn.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInstallStates() {
|
||||||
|
if (!repoContent) return;
|
||||||
|
const installed = getPlugins().map(p => p.url);
|
||||||
|
repoContent.querySelectorAll("[data-link]").forEach(row => {
|
||||||
|
const link = row.getAttribute("data-link");
|
||||||
|
const btn = row.querySelector("button.install-btn");
|
||||||
|
if (!btn) return;
|
||||||
|
if (installed.includes(link)) {
|
||||||
|
btn.textContent = "Installed";
|
||||||
|
btn.disabled = true;
|
||||||
|
} else {
|
||||||
|
btn.textContent = "Install";
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRepo(data, filter = "") {
|
||||||
|
if (!repoContent) return;
|
||||||
|
|
||||||
|
currentRepoData = data.plugins;
|
||||||
|
repoContent.innerHTML = "";
|
||||||
|
|
||||||
|
const filtered = currentRepoData.filter(p =>
|
||||||
|
(p.name + " " + (p.author || "") + " " + (p.description || ""))
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(filter.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
repoContent.innerHTML = `<div style="opacity:0.5;text-align:center;margin-top:30px;">No plugins found.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.forEach(repoPlugin => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;width:100%;min-width:0;";
|
||||||
|
row.setAttribute("data-link", repoPlugin.link);
|
||||||
|
|
||||||
|
const left = document.createElement("div");
|
||||||
|
left.style.cssText = "display:flex;flex-direction:column;flex:1;min-width:0;";
|
||||||
|
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.textContent = `${repoPlugin.name} — ${repoPlugin.author || "Unknown"}`;
|
||||||
|
title.style.cssText = "font-weight:500;word-break:break-word;";
|
||||||
|
|
||||||
|
const desc = document.createElement("div");
|
||||||
|
desc.textContent = repoPlugin.description || "";
|
||||||
|
desc.style.cssText = "font-size:12px;opacity:0.7;word-break:break-word;";
|
||||||
|
|
||||||
|
left.appendChild(title);
|
||||||
|
left.appendChild(desc);
|
||||||
|
|
||||||
|
const installBtn = document.createElement("button");
|
||||||
|
installBtn.className = "install-btn";
|
||||||
|
Object.assign(installBtn.style, {
|
||||||
|
padding: "6px 10px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: "rgba(255,255,255,0.08)",
|
||||||
|
color: "#fff",
|
||||||
|
flexShrink: "0"
|
||||||
|
});
|
||||||
|
|
||||||
|
installBtn.onclick = () => {
|
||||||
|
const plugins = getPlugins();
|
||||||
|
if (!plugins.some(p => p.url === repoPlugin.link)) {
|
||||||
|
plugins.push({ name: repoPlugin.name, url: repoPlugin.link, enabled: false });
|
||||||
|
setPlugins(plugins);
|
||||||
|
window.dispatchEvent(new Event("avia-plugin-list-changed"));
|
||||||
|
triggerManagerRefresh();
|
||||||
|
renderRepo({ plugins: currentRepoData }, searchInput.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
row.appendChild(left);
|
||||||
|
row.appendChild(installBtn);
|
||||||
|
repoContent.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateInstallStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refetchPlugins() {
|
||||||
|
if (!repoContent) return;
|
||||||
|
repoContent.innerHTML = "Loading...";
|
||||||
|
|
||||||
|
function electronFetch() {
|
||||||
|
try {
|
||||||
|
const https = require("https");
|
||||||
|
https.get(OFFICIAL_REPO_URL, res => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", chunk => data += chunk);
|
||||||
|
res.on("end", () => renderRepo(JSON.parse(data)));
|
||||||
|
}).on("error", () => {
|
||||||
|
repoContent.innerHTML = "Failed to fetch repo.";
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
repoContent.innerHTML = "Failed to fetch repo.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fetch(OFFICIAL_REPO_URL)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => renderRepo(data))
|
||||||
|
.catch(() => electronFetch());
|
||||||
|
} catch {
|
||||||
|
electronFetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const THEMES_STORAGE_KEY = "avia_themes";
|
||||||
|
const getStoredThemes = () => JSON.parse(localStorage.getItem(THEMES_STORAGE_KEY) || "[]");
|
||||||
|
const setStoredThemes = (data) => localStorage.setItem(THEMES_STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
function buildThemeCSS(theme, rawCSS) {
|
||||||
|
|
||||||
|
const header = `/* @name ${theme.name}\n @author ${theme.author || "Unknown"}\n @version 1.0\n @description Installed from Trusted Themes Repo\n*/\n`;
|
||||||
|
return header + rawCSS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installThemeCSS(theme, btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Installing…";
|
||||||
|
|
||||||
|
fetch(theme.download)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(rawCSS => {
|
||||||
|
const css = buildThemeCSS(theme, rawCSS);
|
||||||
|
const themes = getStoredThemes();
|
||||||
|
|
||||||
|
const alreadyInstalled = themes.some(t => {
|
||||||
|
const match = t.css.match(/@name\s+(.+)/);
|
||||||
|
return match && match[1].trim() === theme.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (alreadyInstalled) {
|
||||||
|
btn.textContent = "Installed";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
themes.push({ id: crypto.randomUUID(), css, enabled: true });
|
||||||
|
setStoredThemes(themes);
|
||||||
|
|
||||||
|
document.querySelectorAll(".avia-theme-style").forEach(e => e.remove());
|
||||||
|
getStoredThemes().forEach(t => {
|
||||||
|
if (!t.enabled) return;
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.className = "avia-theme-style";
|
||||||
|
style.textContent = t.css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window.__avia_refresh_themes_panel === "function") {
|
||||||
|
window.__avia_refresh_themes_panel();
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.textContent = "Installed";
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
btn.textContent = "Install CSS";
|
||||||
|
btn.disabled = false;
|
||||||
|
alert("Failed to fetch theme CSS.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderThemes(filter = "") {
|
||||||
|
if (!repoContent) return;
|
||||||
|
repoContent.innerHTML = "";
|
||||||
|
|
||||||
|
const filtered = currentThemeData.filter(t =>
|
||||||
|
(t.name + " " + (t.author || ""))
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(filter.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
repoContent.innerHTML = `<div style="opacity:0.5;text-align:center;margin-top:30px;">No themes found.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.forEach(theme => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.style.cssText = "margin-bottom:14px;background:rgba(255,255,255,0.04);border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,0.07);";
|
||||||
|
|
||||||
|
if (theme.preview) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = theme.preview;
|
||||||
|
img.alt = theme.name;
|
||||||
|
img.style.cssText = "width:100%;display:block;background:#111;object-fit:contain;";
|
||||||
|
img.onerror = () => img.style.display = "none";
|
||||||
|
card.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = document.createElement("div");
|
||||||
|
info.style.cssText = "display:flex;justify-content:space-between;align-items:center;padding:10px 12px;gap:8px;";
|
||||||
|
|
||||||
|
const meta = document.createElement("div");
|
||||||
|
meta.style.cssText = "display:flex;flex-direction:column;min-width:0;flex:1;";
|
||||||
|
|
||||||
|
const name = document.createElement("div");
|
||||||
|
name.textContent = theme.name;
|
||||||
|
name.style.cssText = "font-weight:500;word-break:break-word;";
|
||||||
|
|
||||||
|
const author = document.createElement("div");
|
||||||
|
author.textContent = `by ${theme.author || "Unknown"}`;
|
||||||
|
author.style.cssText = "font-size:12px;opacity:0.6;";
|
||||||
|
|
||||||
|
meta.appendChild(name);
|
||||||
|
meta.appendChild(author);
|
||||||
|
|
||||||
|
const alreadyInstalled = getStoredThemes().some(t => {
|
||||||
|
const match = t.css.match(/@name\s+(.+)/);
|
||||||
|
return match && match[1].trim() === theme.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dlBtn = document.createElement("button");
|
||||||
|
dlBtn.textContent = alreadyInstalled ? "Installed" : "Install CSS";
|
||||||
|
dlBtn.disabled = alreadyInstalled;
|
||||||
|
Object.assign(dlBtn.style, {
|
||||||
|
padding: "6px 10px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
cursor: alreadyInstalled ? "default" : "pointer",
|
||||||
|
background: "rgba(255,255,255,0.08)",
|
||||||
|
color: "#fff",
|
||||||
|
flexShrink: "0",
|
||||||
|
fontSize: "12px",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
});
|
||||||
|
dlBtn.onclick = () => installThemeCSS(theme, dlBtn);
|
||||||
|
|
||||||
|
info.appendChild(meta);
|
||||||
|
info.appendChild(dlBtn);
|
||||||
|
card.appendChild(info);
|
||||||
|
repoContent.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refetchThemes() {
|
||||||
|
if (!repoContent) return;
|
||||||
|
repoContent.innerHTML = "Loading themes...";
|
||||||
|
currentThemeData = [];
|
||||||
|
|
||||||
|
fetch(THEMES_REGISTRY_URL)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(async registry => {
|
||||||
|
const sources = registry.sources || [];
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
sources.map(s => fetch(s.url).then(r => r.json()))
|
||||||
|
);
|
||||||
|
results.forEach(r => {
|
||||||
|
if (r.status === "fulfilled") {
|
||||||
|
currentThemeData.push(...(r.value.themes || []));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderThemes(searchInput.value);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (repoContent) repoContent.innerHTML = "Failed to fetch themes.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tab, tabPluginsBtn, tabThemesBtn) {
|
||||||
|
activeTab = tab;
|
||||||
|
const isPlugins = tab === "plugins";
|
||||||
|
|
||||||
|
tabPluginsBtn.style.background = isPlugins ? "rgba(255,255,255,0.12)" : "transparent";
|
||||||
|
tabPluginsBtn.style.color = isPlugins ? "#fff" : "rgba(255,255,255,0.45)";
|
||||||
|
tabThemesBtn.style.background = !isPlugins ? "rgba(255,255,255,0.12)" : "transparent";
|
||||||
|
tabThemesBtn.style.color = !isPlugins ? "#fff" : "rgba(255,255,255,0.45)";
|
||||||
|
|
||||||
|
searchInput.placeholder = isPlugins
|
||||||
|
? "Search plugins, authors, or descriptions"
|
||||||
|
: "Search themes or authors";
|
||||||
|
searchInput.value = "";
|
||||||
|
|
||||||
|
if (isPlugins) {
|
||||||
|
if (currentRepoData.length > 0) renderRepo({ plugins: currentRepoData });
|
||||||
|
else refetchPlugins();
|
||||||
|
} else {
|
||||||
|
if (currentThemeData.length > 0) renderThemes();
|
||||||
|
else refetchThemes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWindow() {
|
||||||
|
let panel = document.getElementById("avia-official-repo-window");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = panel.style.display === "none" ? "flex" : "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "avia-official-repo-window";
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "40px",
|
||||||
|
right: "40px",
|
||||||
|
width: "420px",
|
||||||
|
height: "520px",
|
||||||
|
background: "#1e1e1e",
|
||||||
|
color: "#fff",
|
||||||
|
borderRadius: "20px",
|
||||||
|
boxShadow: "0 12px 35px rgba(0,0,0,0.45)",
|
||||||
|
zIndex: 999999,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.textContent = "Plugins & Themes Repo";
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "18px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "16px",
|
||||||
|
background: "rgba(255,255,255,0.04)",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move",
|
||||||
|
position: "relative",
|
||||||
|
textAlign: "center",
|
||||||
|
userSelect: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
let isDragging = false, offsetX = 0, offsetY = 0;
|
||||||
|
header.addEventListener("mousedown", (e) => {
|
||||||
|
isDragging = true;
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
offsetX = e.clientX - rect.left;
|
||||||
|
offsetY = e.clientY - rect.top;
|
||||||
|
panel.style.bottom = "auto";
|
||||||
|
panel.style.right = "auto";
|
||||||
|
panel.style.left = rect.left + "px";
|
||||||
|
panel.style.top = rect.top + "px";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
panel.style.left = e.clientX - offsetX + "px";
|
||||||
|
panel.style.top = e.clientY - offsetY + "px";
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
isDragging = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = document.createElement("div");
|
||||||
|
close.textContent = "✕";
|
||||||
|
Object.assign(close.style, { position: "absolute", right: "18px", top: "16px", cursor: "pointer" });
|
||||||
|
close.onclick = () => panel.style.display = "none";
|
||||||
|
header.appendChild(close);
|
||||||
|
|
||||||
|
const tabs = document.createElement("div");
|
||||||
|
tabs.style.cssText = "display:flex;gap:6px;padding:10px 12px 0;background:rgba(255,255,255,0.02);border-bottom:1px solid rgba(255,255,255,0.08);";
|
||||||
|
|
||||||
|
const tabStyle = "padding:6px 16px;border-radius:8px 8px 0 0;border:none;cursor:pointer;font-size:13px;font-weight:500;transition:background 0.15s,color 0.15s;font-family:inherit;";
|
||||||
|
|
||||||
|
const tabPluginsBtn = document.createElement("button");
|
||||||
|
tabPluginsBtn.textContent = "Plugins";
|
||||||
|
tabPluginsBtn.style.cssText = tabStyle;
|
||||||
|
|
||||||
|
const tabThemesBtn = document.createElement("button");
|
||||||
|
tabThemesBtn.textContent = "Themes";
|
||||||
|
tabThemesBtn.style.cssText = tabStyle;
|
||||||
|
|
||||||
|
tabPluginsBtn.onclick = () => switchTab("plugins", tabPluginsBtn, tabThemesBtn);
|
||||||
|
tabThemesBtn.onclick = () => switchTab("themes", tabPluginsBtn, tabThemesBtn);
|
||||||
|
|
||||||
|
tabs.appendChild(tabPluginsBtn);
|
||||||
|
tabs.appendChild(tabThemesBtn);
|
||||||
|
|
||||||
|
searchInput = document.createElement("input");
|
||||||
|
searchInput.placeholder = "Search plugins, authors, or descriptions";
|
||||||
|
Object.assign(searchInput.style, {
|
||||||
|
margin: "12px",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
background: "rgba(255,255,255,0.06)",
|
||||||
|
color: "#fff"
|
||||||
|
});
|
||||||
|
searchInput.addEventListener("input", () => {
|
||||||
|
if (activeTab === "plugins") renderRepo({ plugins: currentRepoData }, searchInput.value);
|
||||||
|
else renderThemes(searchInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
repoContent = document.createElement("div");
|
||||||
|
Object.assign(repoContent.style, {
|
||||||
|
flex: "1",
|
||||||
|
overflowY: "auto",
|
||||||
|
overflowX: "hidden",
|
||||||
|
padding: "0 12px 12px"
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
Object.assign(container.style, { flex: "1", display: "flex", flexDirection: "column", overflow: "hidden" });
|
||||||
|
container.appendChild(searchInput);
|
||||||
|
container.appendChild(repoContent);
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(tabs);
|
||||||
|
panel.appendChild(container);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
switchTab("plugins", tabPluginsBtn, tabThemesBtn);
|
||||||
|
refetchPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectSettingsButton() {
|
||||||
|
if (document.getElementById("avia-official-repo-btn-settings")) return;
|
||||||
|
|
||||||
|
const appearanceBtn = [...document.querySelectorAll("a")]
|
||||||
|
.find(a => a.textContent.trim() === "Appearance");
|
||||||
|
const referenceNode = document.getElementById("stoat-fake-quickcss");
|
||||||
|
if (!appearanceBtn || !referenceNode) return;
|
||||||
|
|
||||||
|
const clone = appearanceBtn.cloneNode(true);
|
||||||
|
clone.id = "avia-official-repo-btn-settings";
|
||||||
|
|
||||||
|
const label = [...clone.querySelectorAll("div")].find(d => d.children.length === 0);
|
||||||
|
if (label) label.textContent = "(Sanctum) Plugins/Themes Repo";
|
||||||
|
|
||||||
|
const iconSpan = clone.querySelector("span.material-symbols-outlined");
|
||||||
|
if (iconSpan) {
|
||||||
|
iconSpan.textContent = "extension";
|
||||||
|
iconSpan.style.fontVariationSettings = "'FILL' 0,'wght' 400,'GRAD' 0";
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.onclick = openWindow;
|
||||||
|
referenceNode.parentElement.insertBefore(clone, referenceNode.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("avia-plugin-list-changed", () => {
|
||||||
|
if (document.getElementById("avia-official-repo-window")) {
|
||||||
|
updateInstallStates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new MutationObserver(() => injectSettingsButton())
|
||||||
|
.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
injectSettingsButton();
|
||||||
|
|
||||||
|
})();
|
||||||
522
avia_core/themes.js
Normal file
522
avia_core/themes.js
Normal file
|
|
@ -0,0 +1,522 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
if (window.__AVIA_THEMES_LOADED__) return;
|
||||||
|
window.__AVIA_THEMES_LOADED__ = true;
|
||||||
|
|
||||||
|
const STORAGE_KEY = "avia_themes";
|
||||||
|
let editingThemeId = null;
|
||||||
|
let monacoEditorInstance = null;
|
||||||
|
|
||||||
|
const TEMPLATE = `/*
|
||||||
|
@name Whatever name here
|
||||||
|
@author Whatever Author Here
|
||||||
|
@version 1.0
|
||||||
|
@description Whatever description here
|
||||||
|
*/
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getThemes = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
|
const setThemes = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
|
||||||
|
function preloadMonaco() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (window.monaco) return resolve();
|
||||||
|
const loader = document.createElement("script");
|
||||||
|
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
|
||||||
|
loader.onload = function () {
|
||||||
|
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
|
||||||
|
require(["vs/editor/editor.main"], () => resolve());
|
||||||
|
};
|
||||||
|
document.head.appendChild(loader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMeta(css) {
|
||||||
|
const name = css.match(/@name\s+(.+)/)?.[1] || "Unknown Theme";
|
||||||
|
const author = css.match(/@author\s+(.+)/)?.[1] || "Unknown";
|
||||||
|
const version = css.match(/@version\s+(.+)/)?.[1] || "1.0";
|
||||||
|
const rawDescription = css.match(/@description\s+(.+)/)?.[1] || "No Description Available";
|
||||||
|
const description = rawDescription.trim() === "*/" ? "No Description Available" : rawDescription;
|
||||||
|
return { name, author, version, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFilename(name) {
|
||||||
|
return name
|
||||||
|
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "")
|
||||||
|
.replace(/\s+/g, "_")
|
||||||
|
.replace(/\.+$/, "")
|
||||||
|
.trim() || "theme";
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTheme(theme) {
|
||||||
|
const name = parseMeta(theme.css).name;
|
||||||
|
const filename = sanitizeFilename(name) + ".css";
|
||||||
|
const blob = new Blob([theme.css], { type: "text/css" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyThemes() {
|
||||||
|
document.querySelectorAll(".avia-theme-style").forEach(e => e.remove());
|
||||||
|
getThemes().forEach(theme => {
|
||||||
|
if (!theme.enabled) return;
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.className = "avia-theme-style";
|
||||||
|
style.textContent = theme.css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleBtn(btn, bg) {
|
||||||
|
Object.assign(btn.style, {
|
||||||
|
padding: "5px 12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: bg || "rgba(255,255,255,0.08)",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "12px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontWeight: "500"
|
||||||
|
});
|
||||||
|
btn.onmouseenter = () => btn.style.opacity = "0.75";
|
||||||
|
btn.onmouseleave = () => btn.style.opacity = "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDraggable(panel, handle) {
|
||||||
|
let dragging = false, offsetX, offsetY;
|
||||||
|
handle.addEventListener("mousedown", e => {
|
||||||
|
dragging = true;
|
||||||
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => { dragging = false; document.body.style.userSelect = ""; });
|
||||||
|
document.addEventListener("mousemove", e => {
|
||||||
|
if (!dragging) return;
|
||||||
|
panel.style.left = (e.clientX - offsetX) + "px";
|
||||||
|
panel.style.top = (e.clientY - offsetY) + "px";
|
||||||
|
panel.style.right = "auto";
|
||||||
|
panel.style.bottom = "auto";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openThemeEditor(themeId) {
|
||||||
|
await preloadMonaco();
|
||||||
|
|
||||||
|
editingThemeId = themeId;
|
||||||
|
const themes = getThemes();
|
||||||
|
const theme = themes.find(t => t.id === themeId);
|
||||||
|
if (!theme) return;
|
||||||
|
|
||||||
|
const meta = parseMeta(theme.css);
|
||||||
|
let panel = document.getElementById("avia-theme-editor");
|
||||||
|
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = "flex";
|
||||||
|
panel.querySelector("#avia-theme-editor-title").textContent = "Theme Editor — " + meta.name;
|
||||||
|
if (monacoEditorInstance) {
|
||||||
|
monacoEditorInstance._aviaThemeId = themeId;
|
||||||
|
const model = monacoEditorInstance.getModel();
|
||||||
|
if (model) model.setValue(theme.css || "");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "avia-theme-editor";
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "24px",
|
||||||
|
right: "24px",
|
||||||
|
width: "650px",
|
||||||
|
height: "420px",
|
||||||
|
background: "var(--md-sys-color-surface, #1e1e1e)",
|
||||||
|
color: "var(--md-sys-color-on-surface, #fff)",
|
||||||
|
borderRadius: "16px",
|
||||||
|
boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
|
||||||
|
zIndex: "9999999",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
backdropFilter: "blur(12px)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.id = "avia-theme-editor-title";
|
||||||
|
header.textContent = "Theme Editor — " + meta.name;
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "14px 16px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "14px",
|
||||||
|
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move",
|
||||||
|
color: "#fff",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
makeDraggable(panel, header);
|
||||||
|
|
||||||
|
const close = document.createElement("div");
|
||||||
|
close.textContent = "✕";
|
||||||
|
Object.assign(close.style, {
|
||||||
|
position: "absolute",
|
||||||
|
right: "16px",
|
||||||
|
top: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: "0.6",
|
||||||
|
fontSize: "15px",
|
||||||
|
lineHeight: "1",
|
||||||
|
padding: "2px 4px",
|
||||||
|
color: "#fff"
|
||||||
|
});
|
||||||
|
close.onmouseenter = () => close.style.opacity = "1";
|
||||||
|
close.onmouseleave = () => close.style.opacity = "0.6";
|
||||||
|
close.onclick = () => panel.style.display = "none";
|
||||||
|
|
||||||
|
const editorContainer = document.createElement("div");
|
||||||
|
editorContainer.style.flex = "1";
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(close);
|
||||||
|
panel.appendChild(editorContainer);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
monacoEditorInstance = monaco.editor.create(editorContainer, {
|
||||||
|
value: theme.css || "",
|
||||||
|
language: "css",
|
||||||
|
theme: "vs-dark",
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: "on"
|
||||||
|
});
|
||||||
|
|
||||||
|
monacoEditorInstance._aviaThemeId = themeId;
|
||||||
|
|
||||||
|
monacoEditorInstance.onDidChangeModelContent(() => {
|
||||||
|
const id = monacoEditorInstance._aviaThemeId;
|
||||||
|
if (!id) return;
|
||||||
|
const value = monacoEditorInstance.getValue();
|
||||||
|
const all = getThemes();
|
||||||
|
const target = all.find(t => t.id === id);
|
||||||
|
if (!target) return;
|
||||||
|
target.css = value;
|
||||||
|
setThemes(all);
|
||||||
|
applyThemes();
|
||||||
|
header.textContent = "Theme Editor — " + parseMeta(value).name;
|
||||||
|
if (typeof window.__avia_refresh_themes_panel === "function") {
|
||||||
|
window.__avia_refresh_themes_panel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleThemesPanel() {
|
||||||
|
let panel = document.getElementById("avia-themes-panel");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = panel.style.display === "none" ? "flex" : "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = document.createElement("div");
|
||||||
|
panel.id = "avia-themes-panel";
|
||||||
|
Object.assign(panel.style, {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "40px",
|
||||||
|
right: "40px",
|
||||||
|
width: "500px",
|
||||||
|
height: "460px",
|
||||||
|
background: "var(--md-sys-color-surface, #1e1e1e)",
|
||||||
|
color: "var(--md-sys-color-on-surface, #fff)",
|
||||||
|
borderRadius: "16px",
|
||||||
|
boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
|
||||||
|
zIndex: "999999",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
backdropFilter: "blur(12px)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.textContent = "Themes";
|
||||||
|
Object.assign(header.style, {
|
||||||
|
padding: "14px 16px",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: "14px",
|
||||||
|
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
cursor: "move"
|
||||||
|
});
|
||||||
|
makeDraggable(panel, header);
|
||||||
|
|
||||||
|
const close = document.createElement("div");
|
||||||
|
close.textContent = "✕";
|
||||||
|
Object.assign(close.style, {
|
||||||
|
position: "absolute",
|
||||||
|
right: "16px",
|
||||||
|
top: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: "0.6",
|
||||||
|
fontSize: "15px",
|
||||||
|
lineHeight: "1",
|
||||||
|
padding: "2px 4px"
|
||||||
|
});
|
||||||
|
close.onmouseenter = () => close.style.opacity = "1";
|
||||||
|
close.onmouseleave = () => close.style.opacity = "0.6";
|
||||||
|
close.onclick = () => panel.style.display = "none";
|
||||||
|
|
||||||
|
const btnRow = document.createElement("div");
|
||||||
|
Object.assign(btnRow.style, {
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
padding: "12px 16px",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
const importBtn = document.createElement("button");
|
||||||
|
importBtn.textContent = "Import Theme";
|
||||||
|
styleBtn(importBtn);
|
||||||
|
importBtn.style.flex = "1";
|
||||||
|
importBtn.style.padding = "8px 12px";
|
||||||
|
|
||||||
|
const newBtn = document.createElement("button");
|
||||||
|
newBtn.textContent = "+ New";
|
||||||
|
styleBtn(newBtn);
|
||||||
|
newBtn.style.flex = "1";
|
||||||
|
newBtn.style.padding = "8px 12px";
|
||||||
|
|
||||||
|
btnRow.appendChild(importBtn);
|
||||||
|
btnRow.appendChild(newBtn);
|
||||||
|
|
||||||
|
const list = document.createElement("div");
|
||||||
|
Object.assign(list.style, {
|
||||||
|
flex: "1",
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: "16px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "8px"
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropOverlay = document.createElement("div");
|
||||||
|
dropOverlay.textContent = "Drop .css or .txt files here";
|
||||||
|
Object.assign(dropOverlay.style, {
|
||||||
|
position: "absolute",
|
||||||
|
inset: "0",
|
||||||
|
background: "rgba(0,0,0,0.6)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#fff",
|
||||||
|
opacity: "0",
|
||||||
|
pointerEvents: "none",
|
||||||
|
transition: "opacity 0.15s ease",
|
||||||
|
borderRadius: "16px"
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(close);
|
||||||
|
panel.appendChild(btnRow);
|
||||||
|
panel.appendChild(list);
|
||||||
|
panel.appendChild(dropOverlay);
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
let dragDepth = 0;
|
||||||
|
|
||||||
|
panel.addEventListener("dragenter", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dragDepth++;
|
||||||
|
dropOverlay.style.opacity = "1";
|
||||||
|
panel.style.border = "1px dashed rgba(255,255,255,0.4)";
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("dragover", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("dragleave", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dragDepth--;
|
||||||
|
if (dragDepth <= 0) {
|
||||||
|
dropOverlay.style.opacity = "0";
|
||||||
|
panel.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||||
|
dragDepth = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("drop", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dropOverlay.style.opacity = "0";
|
||||||
|
panel.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||||
|
dragDepth = 0;
|
||||||
|
const files = [...e.dataTransfer.files].filter(f => f.name.endsWith(".css") || f.name.endsWith(".txt"));
|
||||||
|
if (!files.length) return;
|
||||||
|
const themes = getThemes();
|
||||||
|
for (const file of files) {
|
||||||
|
const css = await file.text();
|
||||||
|
themes.push({ id: crypto.randomUUID(), css, enabled: true });
|
||||||
|
}
|
||||||
|
setThemes(themes);
|
||||||
|
applyThemes();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
list.innerHTML = "";
|
||||||
|
const themes = getThemes();
|
||||||
|
|
||||||
|
if (themes.length === 0) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.textContent = "No themes yet. Import or create one above.";
|
||||||
|
Object.assign(empty.style, { opacity: "0.4", fontSize: "13px" });
|
||||||
|
list.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
themes.forEach(theme => {
|
||||||
|
const meta = parseMeta(theme.css);
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
Object.assign(card.style, {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px 12px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
background: "rgba(255,255,255,0.04)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.06)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const left = document.createElement("div");
|
||||||
|
Object.assign(left.style, { display: "flex", alignItems: "center", gap: "10px" });
|
||||||
|
|
||||||
|
const dot = document.createElement("div");
|
||||||
|
Object.assign(dot.style, {
|
||||||
|
width: "10px",
|
||||||
|
height: "10px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
flexShrink: "0",
|
||||||
|
background: theme.enabled ? "#4dff88" : "#777",
|
||||||
|
boxShadow: theme.enabled ? "0 0 6px #4dff88" : "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = document.createElement("div");
|
||||||
|
info.innerHTML = `<div style="font-weight:600;font-size:13px">${meta.name}</div><div style="font-size:11px;opacity:.5">${meta.author} • v${meta.version}</div><div style="font-size:11px;opacity:.4">${meta.description}</div>`;
|
||||||
|
|
||||||
|
left.appendChild(dot);
|
||||||
|
left.appendChild(info);
|
||||||
|
|
||||||
|
const controls = document.createElement("div");
|
||||||
|
Object.assign(controls.style, { display: "flex", gap: "6px" });
|
||||||
|
|
||||||
|
const toggle = document.createElement("button");
|
||||||
|
toggle.textContent = theme.enabled ? "Disable" : "Enable";
|
||||||
|
styleBtn(toggle);
|
||||||
|
toggle.onclick = () => {
|
||||||
|
theme.enabled = !theme.enabled;
|
||||||
|
setThemes(themes);
|
||||||
|
applyThemes();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const edit = document.createElement("button");
|
||||||
|
edit.textContent = "Edit";
|
||||||
|
styleBtn(edit, "rgba(100,160,255,0.15)");
|
||||||
|
edit.onclick = () => openThemeEditor(theme.id);
|
||||||
|
|
||||||
|
const dlBtn = document.createElement("button");
|
||||||
|
dlBtn.textContent = "Export";
|
||||||
|
styleBtn(dlBtn, "rgba(80,200,120,0.15)");
|
||||||
|
dlBtn.title = "Download theme as .css";
|
||||||
|
dlBtn.onclick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
downloadTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const del = document.createElement("button");
|
||||||
|
del.textContent = "✕";
|
||||||
|
styleBtn(del, "rgba(255,80,80,0.15)");
|
||||||
|
del.onclick = () => {
|
||||||
|
const updated = themes.filter(t => t.id !== theme.id);
|
||||||
|
setThemes(updated);
|
||||||
|
applyThemes();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
controls.appendChild(toggle);
|
||||||
|
controls.appendChild(edit);
|
||||||
|
controls.appendChild(dlBtn);
|
||||||
|
controls.appendChild(del);
|
||||||
|
card.appendChild(left);
|
||||||
|
card.appendChild(controls);
|
||||||
|
list.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__avia_refresh_themes_panel = render;
|
||||||
|
|
||||||
|
importBtn.onclick = () => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = ".css,.txt";
|
||||||
|
input.multiple = true;
|
||||||
|
input.onchange = async () => {
|
||||||
|
const files = [...input.files];
|
||||||
|
if (!files.length) return;
|
||||||
|
const themes = getThemes();
|
||||||
|
for (const file of files) {
|
||||||
|
const css = await file.text();
|
||||||
|
themes.push({ id: crypto.randomUUID(), css, enabled: true });
|
||||||
|
}
|
||||||
|
setThemes(themes);
|
||||||
|
applyThemes();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
newBtn.onclick = () => {
|
||||||
|
const themes = getThemes();
|
||||||
|
themes.push({ id: crypto.randomUUID(), css: TEMPLATE, enabled: true });
|
||||||
|
setThemes(themes);
|
||||||
|
applyThemes();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectButton() {
|
||||||
|
if (document.getElementById("avia-themes-btn")) return;
|
||||||
|
const appearanceBtn = [...document.querySelectorAll("a")].find(a => a.textContent.trim() === "Appearance");
|
||||||
|
const quickCSS = document.getElementById("stoat-fake-quickcss");
|
||||||
|
if (!appearanceBtn || !quickCSS) return;
|
||||||
|
const clone = appearanceBtn.cloneNode(true);
|
||||||
|
clone.id = "avia-themes-btn";
|
||||||
|
const text = [...clone.querySelectorAll("div")].find(d => d.children.length === 0);
|
||||||
|
if (text) text.textContent = "(Sanctum) Themes";
|
||||||
|
clone.onclick = toggleThemesPanel;
|
||||||
|
quickCSS.parentElement.insertBefore(clone, quickCSS.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
new MutationObserver(injectButton).observe(document.body, { childList: true, subtree: true });
|
||||||
|
injectButton();
|
||||||
|
applyThemes();
|
||||||
|
preloadMonaco();
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<caption>Main window</caption>
|
<caption>Main window</caption>
|
||||||
<image>screenshot.png</image>
|
<image>https://raw.githubusercontent.com/stoatchat/for-desktop/b57faa2c59865fea15a879c9a9304271067d0020/screenshot.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<releases>
|
<releases>
|
||||||
|
|
|
||||||
10
cloud.mithraic.sanctum.desktop
Normal file
10
cloud.mithraic.sanctum.desktop
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Sanctum
|
||||||
|
Comment=Open source, user-first chat platform
|
||||||
|
Exec=sanctum
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Icon=cloud.mithraic.sanctum
|
||||||
|
Categories=Network;InstantMessaging
|
||||||
|
StartupWMClass=sanctum
|
||||||
|
X-Desktop-File-Install-Version=0.26
|
||||||
41
cloud.mithraic.sanctum.metainfo.xml
Normal file
41
cloud.mithraic.sanctum.metainfo.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop">
|
||||||
|
<id>cloud.mithraic.sanctum</id>
|
||||||
|
<launchable type="desktop-id">cloud.mithraic.sanctum.desktop</launchable>
|
||||||
|
<name>Sanctum</name>
|
||||||
|
<developer id="cloud.mithraic">
|
||||||
|
<name>izzy</name>
|
||||||
|
</developer>
|
||||||
|
<summary>Open source, user-first chat platform</summary>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>AGPL-3.0</project_license>
|
||||||
|
<description>
|
||||||
|
<p>Sanctum is a self-hosted Stoat client with Avia Client mod support.</p>
|
||||||
|
</description>
|
||||||
|
<content_rating type="oars-1.1">
|
||||||
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
|
<content_attribute id="social-info">intense</content_attribute>
|
||||||
|
<content_attribute id="social-audio">intense</content_attribute>
|
||||||
|
<content_attribute id="social-contacts">intense</content_attribute>
|
||||||
|
</content_rating>
|
||||||
|
<requires>
|
||||||
|
<display_length compare="ge">940</display_length>
|
||||||
|
<internet>always</internet>
|
||||||
|
</requires>
|
||||||
|
<supports>
|
||||||
|
<control>keyboard</control>
|
||||||
|
<control>pointing</control>
|
||||||
|
</supports>
|
||||||
|
<releases>
|
||||||
|
<release date="2026-05-05" version="1.0.7">
|
||||||
|
<description>
|
||||||
|
<p>Fixed a main-process bootstrap race and improved VC sounds / game presence behavior.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release date="2026-04-22" version="1.0.0">
|
||||||
|
<description>
|
||||||
|
<p>Initial Sanctum release based on Avia Client with self-hosted instance support.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
</component>
|
||||||
106
forge.config.ts
106
forge.config.ts
|
|
@ -6,25 +6,27 @@ import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
||||||
import { MakerZIP } from "@electron-forge/maker-zip";
|
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||||
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
||||||
import { VitePlugin } from "@electron-forge/plugin-vite";
|
import { VitePlugin } from "@electron-forge/plugin-vite";
|
||||||
import { PublisherGithub } from "@electron-forge/publisher-github";
|
import { VitePluginBuildConfig } from "@electron-forge/plugin-vite/dist/Config";
|
||||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||||
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
const STRINGS = {
|
const STRINGS = {
|
||||||
author: "Revolt Platforms LTD",
|
author: "izzy",
|
||||||
name: "Stoat",
|
name: "Sanctum",
|
||||||
execName: "stoat-desktop",
|
execName: "sanctum",
|
||||||
description: "Open source user-first chat platform.",
|
description: "Open source user-first chat platform.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ASSET_DIR = "assets/desktop";
|
const ASSET_DIR = "assets/desktop";
|
||||||
|
const AVIA_ASSET_DIR = "avia_assets";
|
||||||
|
|
||||||
const makers: ForgeConfig["makers"] = [
|
const makers: ForgeConfig["makers"] = [
|
||||||
new MakerSquirrel({
|
new MakerSquirrel({
|
||||||
name: STRINGS.name,
|
name: STRINGS.name,
|
||||||
authors: STRINGS.author,
|
authors: STRINGS.author,
|
||||||
iconUrl: `https://stoat.chat/app/assets/icon-DUSNE-Pb.ico`,
|
iconUrl: `https://stoat.chat/app/assets/icon-DUSNE-Pb.ico`,
|
||||||
setupIcon: `${ASSET_DIR}/icon.ico`,
|
setupIcon: `${AVIA_ASSET_DIR}/icon.ico`,
|
||||||
description: STRINGS.description,
|
description: STRINGS.description,
|
||||||
exe: `${STRINGS.execName}.exe`,
|
exe: `${STRINGS.execName}.exe`,
|
||||||
setupExe: `${STRINGS.execName}-setup.exe`,
|
setupExe: `${STRINGS.execName}-setup.exe`,
|
||||||
|
|
@ -42,7 +44,7 @@ if (!process.env.PLATFORM) {
|
||||||
}),
|
}),
|
||||||
new MakerFlatpak({
|
new MakerFlatpak({
|
||||||
options: {
|
options: {
|
||||||
id: "chat.stoat.stoat-desktop",
|
id: "cloud.mithraic.sanctum",
|
||||||
description: STRINGS.description,
|
description: STRINGS.description,
|
||||||
productName: STRINGS.name,
|
productName: STRINGS.name,
|
||||||
productDescription: STRINGS.description,
|
productDescription: STRINGS.description,
|
||||||
|
|
@ -87,65 +89,61 @@ if (!process.env.PLATFORM) {
|
||||||
productName: STRINGS.name,
|
productName: STRINGS.name,
|
||||||
productDescription: STRINGS.description,
|
productDescription: STRINGS.description,
|
||||||
categories: ["Network"],
|
categories: ["Network"],
|
||||||
icon: `${ASSET_DIR}/icon.png`,
|
icon: `${AVIA_ASSET_DIR}/icon.png`,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customVitePluginBuild: VitePluginBuildConfig[] = [
|
||||||
|
{
|
||||||
|
entry: "avia_assets/icon.png",
|
||||||
|
config: "vite.main.config.ts",
|
||||||
|
target: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: "about.html",
|
||||||
|
config: "vite.main.config.ts",
|
||||||
|
target: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: "src/main.ts",
|
||||||
|
config: "vite.main.config.ts",
|
||||||
|
target: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: "src/preload.ts",
|
||||||
|
config: "vite.preload.config.ts",
|
||||||
|
target: "preload",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
fs.readdir("avia_core", (err: NodeJS.ErrnoException, files: string[]) => {
|
||||||
|
if (err) return;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (["js", "ts", "tsx"].includes(file.split(".").pop().toLowerCase())) {
|
||||||
|
customVitePluginBuild.push({
|
||||||
|
entry: `avia_core/${file}`,
|
||||||
|
config: "vite.main.config.ts",
|
||||||
|
target: "main",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const config: ForgeConfig = {
|
const config: ForgeConfig = {
|
||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
asar: true,
|
asar: true,
|
||||||
name: STRINGS.name,
|
name: STRINGS.name,
|
||||||
executableName: STRINGS.execName,
|
executableName: STRINGS.execName,
|
||||||
icon: `${ASSET_DIR}/icon`,
|
icon: `${AVIA_ASSET_DIR}/icon`,
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
makers,
|
makers,
|
||||||
plugins: [
|
plugins: [
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
build: [
|
build: customVitePluginBuild,
|
||||||
{
|
|
||||||
entry: "src/main.ts",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/preload.ts",
|
|
||||||
config: "vite.preload.config.ts",
|
|
||||||
target: "preload",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/inject.js",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/aviaclientcategory.js",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/themes.js",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/aviafavsystem.js",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/pluginsupport.js",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: "src/aviaversion.js",
|
|
||||||
config: "vite.main.config.ts",
|
|
||||||
target: "main",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
renderer: [],
|
renderer: [],
|
||||||
}),
|
}),
|
||||||
new FusesPlugin({
|
new FusesPlugin({
|
||||||
|
|
@ -158,14 +156,6 @@ const config: ForgeConfig = {
|
||||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
publishers: [
|
|
||||||
new PublisherGithub({
|
|
||||||
repository: {
|
|
||||||
owner: "stoatchat",
|
|
||||||
name: "for-desktop",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
15
package.json
15
package.json
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "stoat-desktop",
|
"name": "sanctum",
|
||||||
"productName": "stoat-desktop",
|
"productName": "Sanctum",
|
||||||
"version": "1.3.0",
|
"version": "1.0.7",
|
||||||
|
"aviaVersion": "1.0.7",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"repository": "stoatchat/desktop",
|
"repository": "https://git.mithraic.cloud/ad3laid3/sanctum",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
|
|
@ -37,6 +38,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"electron": "38.1.2",
|
"electron": "38.1.2",
|
||||||
|
"electron-vite": "^5.0.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"json-schema-typed": "^8.0.1",
|
"json-schema-typed": "^8.0.1",
|
||||||
|
|
@ -52,8 +54,7 @@
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"update-electron-app": "^3.1.1",
|
|
||||||
"utf-8-validate": "^6.0.5"
|
"utf-8-validate": "^6.0.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
"packageManager": "pnpm@10.33.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
598
pnpm-lock.yaml
generated
598
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 702 KiB After Width: | Height: | Size: 467 KiB |
|
|
@ -1,34 +0,0 @@
|
||||||
(function(){
|
|
||||||
if(window.__AVIA_CATEGORY_SETTINGS__) return;
|
|
||||||
window.__AVIA_CATEGORY_SETTINGS__ = true;
|
|
||||||
|
|
||||||
function inject(){
|
|
||||||
|
|
||||||
if(document.getElementById('avia-cloned-settings')) return;
|
|
||||||
|
|
||||||
const spans = [...document.querySelectorAll('span')];
|
|
||||||
const target = spans.find(s => s.textContent.trim() === "User Settings");
|
|
||||||
if(!target) return;
|
|
||||||
|
|
||||||
const container = target.closest('.d_flex.flex-d_column');
|
|
||||||
if(!container) return;
|
|
||||||
|
|
||||||
const clone = container.cloneNode(true);
|
|
||||||
clone.id = "avia-cloned-settings";
|
|
||||||
|
|
||||||
const header = clone.querySelector('span');
|
|
||||||
if(header) header.textContent = "AVIA CLIENT SETTINGS";
|
|
||||||
|
|
||||||
const list = clone.querySelector('.d_flex.flex-d_column.gap_var\\(--gap-s\\)');
|
|
||||||
if(list) list.innerHTML = "";
|
|
||||||
|
|
||||||
container.parentNode.insertBefore(clone, container.nextSibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
new MutationObserver(() => {
|
|
||||||
inject();
|
|
||||||
}).observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
inject();
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@ -1,349 +0,0 @@
|
||||||
(function () {
|
|
||||||
|
|
||||||
if (window.__AVIA_FAVORITES_LOADED__) return;
|
|
||||||
window.__AVIA_FAVORITES_LOADED__ = true;
|
|
||||||
|
|
||||||
const STORAGE_KEY = "avia_favorites";
|
|
||||||
|
|
||||||
const getFavorites = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
|
||||||
const setFavorites = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
||||||
|
|
||||||
function extractYouTubeID(url) {
|
|
||||||
const reg = /(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^&?/]+)/;
|
|
||||||
const match = url.match(reg);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFavoritesPanel() {
|
|
||||||
|
|
||||||
let panel = document.getElementById("avia-favorites-panel");
|
|
||||||
if (panel) {
|
|
||||||
panel.style.display = panel.style.display === "none" ? "flex" : "none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
panel = document.createElement("div");
|
|
||||||
panel.id = "avia-favorites-panel";
|
|
||||||
|
|
||||||
Object.assign(panel.style, {
|
|
||||||
position: "fixed",
|
|
||||||
bottom: "40px",
|
|
||||||
right: "40px",
|
|
||||||
width: "640px",
|
|
||||||
height: "580px",
|
|
||||||
background: "#1e1e1e",
|
|
||||||
color: "#fff",
|
|
||||||
borderRadius: "20px",
|
|
||||||
boxShadow: "0 12px 35px rgba(0,0,0,0.45)",
|
|
||||||
zIndex: 999999,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflow: "hidden",
|
|
||||||
border: "1px solid rgba(255,255,255,0.08)"
|
|
||||||
});
|
|
||||||
|
|
||||||
const header = document.createElement("div");
|
|
||||||
header.textContent = "Favorites";
|
|
||||||
Object.assign(header.style, {
|
|
||||||
padding: "18px",
|
|
||||||
fontWeight: "600",
|
|
||||||
fontSize: "16px",
|
|
||||||
background: "rgba(255,255,255,0.04)",
|
|
||||||
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
|
||||||
cursor: "move",
|
|
||||||
position: "relative",
|
|
||||||
userSelect: "none"
|
|
||||||
});
|
|
||||||
|
|
||||||
const close = document.createElement("div");
|
|
||||||
close.textContent = "✕";
|
|
||||||
Object.assign(close.style, {
|
|
||||||
position: "absolute",
|
|
||||||
right: "18px",
|
|
||||||
top: "16px",
|
|
||||||
cursor: "pointer"
|
|
||||||
});
|
|
||||||
close.onclick = () => panel.style.display = "none";
|
|
||||||
header.appendChild(close);
|
|
||||||
|
|
||||||
const inputRow = document.createElement("div");
|
|
||||||
Object.assign(inputRow.style, {
|
|
||||||
display: "flex",
|
|
||||||
gap: "8px",
|
|
||||||
padding: "14px 18px"
|
|
||||||
});
|
|
||||||
|
|
||||||
const urlInput = document.createElement("input");
|
|
||||||
urlInput.placeholder = "Paste link...";
|
|
||||||
Object.assign(urlInput.style, {
|
|
||||||
flex: "2",
|
|
||||||
padding: "10px",
|
|
||||||
borderRadius: "10px",
|
|
||||||
border: "none",
|
|
||||||
outline: "none"
|
|
||||||
});
|
|
||||||
|
|
||||||
const titleInput = document.createElement("input");
|
|
||||||
titleInput.placeholder = "Optional title...";
|
|
||||||
Object.assign(titleInput.style, {
|
|
||||||
flex: "1",
|
|
||||||
padding: "10px",
|
|
||||||
borderRadius: "10px",
|
|
||||||
border: "none",
|
|
||||||
outline: "none"
|
|
||||||
});
|
|
||||||
|
|
||||||
const addBtn = document.createElement("button");
|
|
||||||
addBtn.textContent = "Add";
|
|
||||||
Object.assign(addBtn.style, {
|
|
||||||
padding: "10px 16px",
|
|
||||||
borderRadius: "10px",
|
|
||||||
border: "none",
|
|
||||||
cursor: "pointer"
|
|
||||||
});
|
|
||||||
|
|
||||||
inputRow.appendChild(urlInput);
|
|
||||||
inputRow.appendChild(titleInput);
|
|
||||||
inputRow.appendChild(addBtn);
|
|
||||||
|
|
||||||
const grid = document.createElement("div");
|
|
||||||
Object.assign(grid.style, {
|
|
||||||
flex: "1",
|
|
||||||
minHeight: "0",
|
|
||||||
overflowY: "auto",
|
|
||||||
padding: "18px",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(auto-fill, 120px)",
|
|
||||||
gap: "14px",
|
|
||||||
alignContent: "start"
|
|
||||||
});
|
|
||||||
|
|
||||||
panel.appendChild(header);
|
|
||||||
panel.appendChild(inputRow);
|
|
||||||
panel.appendChild(grid);
|
|
||||||
document.body.appendChild(panel);
|
|
||||||
|
|
||||||
let isDragging = false, offsetX, offsetY;
|
|
||||||
|
|
||||||
header.addEventListener("mousedown", e => {
|
|
||||||
isDragging = true;
|
|
||||||
offsetX = e.clientX - panel.offsetLeft;
|
|
||||||
offsetY = e.clientY - panel.offsetTop;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mouseup", () => isDragging = false);
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", e => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
panel.style.left = (e.clientX - offsetX) + "px";
|
|
||||||
panel.style.top = (e.clientY - offsetY) + "px";
|
|
||||||
panel.style.right = "auto";
|
|
||||||
panel.style.bottom = "auto";
|
|
||||||
});
|
|
||||||
|
|
||||||
function showToast(card) {
|
|
||||||
const toast = document.createElement("div");
|
|
||||||
toast.textContent = "Copied to clipboard";
|
|
||||||
Object.assign(toast.style, {
|
|
||||||
position: "absolute",
|
|
||||||
bottom: "6px",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
background: "rgba(0,0,0,0.85)",
|
|
||||||
padding: "6px 10px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontSize: "11px",
|
|
||||||
opacity: "0",
|
|
||||||
transition: "opacity 0.2s",
|
|
||||||
pointerEvents: "none"
|
|
||||||
});
|
|
||||||
card.appendChild(toast);
|
|
||||||
requestAnimationFrame(() => toast.style.opacity = "1");
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.opacity = "0";
|
|
||||||
setTimeout(() => toast.remove(), 200);
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallbackCopy(text) {
|
|
||||||
const textarea = document.createElement("textarea");
|
|
||||||
textarea.value = text;
|
|
||||||
textarea.style.position = "fixed";
|
|
||||||
textarea.style.opacity = "0";
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.focus();
|
|
||||||
textarea.select();
|
|
||||||
try { document.execCommand("copy"); } catch {}
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
|
|
||||||
grid.innerHTML = "";
|
|
||||||
const favorites = getFavorites();
|
|
||||||
|
|
||||||
favorites.forEach(item => {
|
|
||||||
|
|
||||||
const card = document.createElement("div");
|
|
||||||
Object.assign(card.style, {
|
|
||||||
position: "relative",
|
|
||||||
width: "120px",
|
|
||||||
height: "120px",
|
|
||||||
borderRadius: "14px",
|
|
||||||
overflow: "hidden",
|
|
||||||
background: "rgba(255,255,255,0.05)",
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center"
|
|
||||||
});
|
|
||||||
|
|
||||||
const remove = document.createElement("div");
|
|
||||||
remove.textContent = "✕";
|
|
||||||
Object.assign(remove.style, {
|
|
||||||
position: "absolute",
|
|
||||||
top: "6px",
|
|
||||||
right: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
cursor: "pointer",
|
|
||||||
background: "rgba(0,0,0,0.6)",
|
|
||||||
padding: "2px 6px",
|
|
||||||
borderRadius: "6px",
|
|
||||||
zIndex: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
remove.onclick = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setFavorites(favorites.filter(f => f.url !== item.url));
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
card.appendChild(remove);
|
|
||||||
|
|
||||||
let mediaAdded = false;
|
|
||||||
|
|
||||||
const ytID = extractYouTubeID(item.url);
|
|
||||||
if (ytID) {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = `https://img.youtube.com/vi/${ytID}/hqdefault.jpg`;
|
|
||||||
Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" });
|
|
||||||
card.appendChild(img);
|
|
||||||
mediaAdded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaAdded) {
|
|
||||||
const ext = item.url.split(".").pop().split("?")[0].toLowerCase();
|
|
||||||
const isVideo = ["mp4","webm","mov","gifv"].includes(ext);
|
|
||||||
|
|
||||||
if (isVideo) {
|
|
||||||
const video = document.createElement("video");
|
|
||||||
video.src = item.url.replace(".gifv",".mp4");
|
|
||||||
video.autoplay = true;
|
|
||||||
video.loop = true;
|
|
||||||
video.muted = true;
|
|
||||||
video.playsInline = true;
|
|
||||||
Object.assign(video.style, { width:"100%", height:"100%", objectFit:"cover" });
|
|
||||||
video.onerror = fallback;
|
|
||||||
card.appendChild(video);
|
|
||||||
} else {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = item.url;
|
|
||||||
Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" });
|
|
||||||
img.onerror = fallback;
|
|
||||||
card.appendChild(img);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallback() {
|
|
||||||
card.innerHTML = "";
|
|
||||||
card.appendChild(remove);
|
|
||||||
const text = document.createElement("div");
|
|
||||||
text.textContent = item.title || item.url;
|
|
||||||
Object.assign(text.style, {
|
|
||||||
padding:"8px",
|
|
||||||
fontSize:"11px",
|
|
||||||
textAlign:"center",
|
|
||||||
wordBreak:"break-word"
|
|
||||||
});
|
|
||||||
card.appendChild(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.title) {
|
|
||||||
const titleOverlay = document.createElement("div");
|
|
||||||
titleOverlay.textContent = item.title;
|
|
||||||
Object.assign(titleOverlay.style, {
|
|
||||||
position:"absolute",
|
|
||||||
bottom:"0",
|
|
||||||
width:"100%",
|
|
||||||
background:"rgba(0,0,0,0.6)",
|
|
||||||
fontSize:"11px",
|
|
||||||
padding:"4px",
|
|
||||||
textAlign:"center",
|
|
||||||
whiteSpace:"nowrap",
|
|
||||||
overflow:"hidden",
|
|
||||||
textOverflow:"ellipsis"
|
|
||||||
});
|
|
||||||
card.appendChild(titleOverlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
card.onclick = () => {
|
|
||||||
const doToast = () => showToast(card);
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(item.url)
|
|
||||||
.then(doToast)
|
|
||||||
.catch(() => {
|
|
||||||
fallbackCopy(item.url);
|
|
||||||
doToast();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fallbackCopy(item.url);
|
|
||||||
doToast();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
grid.appendChild(card);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addBtn.onclick = () => {
|
|
||||||
const url = urlInput.value.trim();
|
|
||||||
const title = titleInput.value.trim();
|
|
||||||
if (!url) return;
|
|
||||||
const favorites = getFavorites();
|
|
||||||
if (favorites.some(f => f.url === url)) return;
|
|
||||||
favorites.push({ url, title, addedAt: Date.now() });
|
|
||||||
setFavorites(favorites);
|
|
||||||
urlInput.value = "";
|
|
||||||
titleInput.value = "";
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectButton() {
|
|
||||||
|
|
||||||
if (document.getElementById("avia-favorites-btn")) return;
|
|
||||||
|
|
||||||
const gifSpan = [...document.querySelectorAll("span.material-symbols-outlined")]
|
|
||||||
.find(s => s.textContent.trim() === "gif");
|
|
||||||
|
|
||||||
if (!gifSpan) return;
|
|
||||||
|
|
||||||
const wrapper = gifSpan.closest("div.flex-sh_0");
|
|
||||||
if (!wrapper) return;
|
|
||||||
|
|
||||||
const clone = wrapper.cloneNode(true);
|
|
||||||
clone.id = "avia-favorites-btn";
|
|
||||||
clone.querySelector("span.material-symbols-outlined").textContent = "star";
|
|
||||||
clone.querySelector("button").onclick = toggleFavoritesPanel;
|
|
||||||
|
|
||||||
wrapper.parentElement.insertBefore(clone, wrapper.nextSibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
new MutationObserver(injectButton)
|
|
||||||
.observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
injectButton();
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
(function () {
|
|
||||||
|
|
||||||
if (window.__AVIA_VERSION_PATCH__) return;
|
|
||||||
window.__AVIA_VERSION_PATCH__ = true;
|
|
||||||
|
|
||||||
function patchVersion() {
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll("span.lh_1rem.fs_0\\.75rem.ls_0\\.03125rem.fw_500")
|
|
||||||
.forEach(el => {
|
|
||||||
|
|
||||||
if (el.dataset.aviaPatched) return;
|
|
||||||
|
|
||||||
const match = el.textContent.match(/Stoat for Desktop\s+([0-9.]+)/);
|
|
||||||
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
const stoatVersion = match[1];
|
|
||||||
|
|
||||||
el.dataset.aviaPatched = "true";
|
|
||||||
|
|
||||||
el.innerHTML = `
|
|
||||||
Avia Client Desktop<br>
|
|
||||||
<span style="font-size:10px;opacity:0.7;">
|
|
||||||
Based on Stoat ${stoatVersion}
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new MutationObserver(patchVersion);
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
|
|
||||||
patchVersion();
|
|
||||||
|
|
||||||
})();
|
|
||||||
67
src/config.d.ts
vendored
67
src/config.d.ts
vendored
|
|
@ -1,10 +1,15 @@
|
||||||
declare type DesktopConfig = {
|
declare type DesktopConfig = {
|
||||||
firstLaunch: boolean;
|
firstLaunch: boolean;
|
||||||
customFrame: boolean;
|
customFrame: boolean;
|
||||||
|
customFrameNativeMenu: boolean;
|
||||||
minimiseToTray: boolean;
|
minimiseToTray: boolean;
|
||||||
|
disableTrayClick: boolean;
|
||||||
spellchecker: boolean;
|
spellchecker: boolean;
|
||||||
hardwareAcceleration: boolean;
|
hardwareAcceleration: boolean;
|
||||||
discordRpc: boolean;
|
discordRpc: boolean;
|
||||||
|
gamePresenceEnabled: boolean;
|
||||||
|
gamePresenceRestrictToAllowList: boolean;
|
||||||
|
gamePresenceAllowList: string;
|
||||||
windowState: {
|
windowState: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -13,3 +18,65 @@ declare type DesktopConfig = {
|
||||||
isMaximised: boolean;
|
isMaximised: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare type VoiceOverlayMember = {
|
||||||
|
name: string;
|
||||||
|
speaking?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
deafened?: boolean;
|
||||||
|
avatarUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type VoiceOverlayState = {
|
||||||
|
channelName?: string;
|
||||||
|
isInCall: boolean;
|
||||||
|
members: VoiceOverlayMember[];
|
||||||
|
selfMuted?: boolean;
|
||||||
|
selfDeafened?: boolean;
|
||||||
|
source?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type SanctumGamePresence = {
|
||||||
|
title: string;
|
||||||
|
processName: string;
|
||||||
|
startedAt: number;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type SanctumActivityState = {
|
||||||
|
game: SanctumGamePresence | null;
|
||||||
|
voice: VoiceOverlayState | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
native: {
|
||||||
|
versions: {
|
||||||
|
node: () => string;
|
||||||
|
chrome: () => string;
|
||||||
|
electron: () => string;
|
||||||
|
desktop: () => string;
|
||||||
|
aviaClient: () => string;
|
||||||
|
};
|
||||||
|
overlay: {
|
||||||
|
setVoiceState: (state: VoiceOverlayState | null) => void;
|
||||||
|
};
|
||||||
|
activity: {
|
||||||
|
getState: () => Promise<SanctumActivityState>;
|
||||||
|
onUpdate: (callback: (state: SanctumActivityState) => void) => () => void;
|
||||||
|
debugSetState: (state: SanctumActivityState) => Promise<SanctumActivityState>;
|
||||||
|
};
|
||||||
|
minimise: () => void;
|
||||||
|
maximise: () => void;
|
||||||
|
close: () => void;
|
||||||
|
setBadgeCount: (count: number) => void;
|
||||||
|
};
|
||||||
|
desktopConfig: {
|
||||||
|
get: () => DesktopConfig;
|
||||||
|
set: (config: DesktopConfig) => void;
|
||||||
|
getAutostart: () => Promise<boolean>;
|
||||||
|
setAutostart: (value: boolean) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
4
src/hackfix.d.ts
vendored
Normal file
4
src/hackfix.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
declare module "*?asset" {
|
||||||
|
export const assetURL: string;
|
||||||
|
export default assetURL;
|
||||||
|
}
|
||||||
135
src/main.ts
135
src/main.ts
|
|
@ -1,16 +1,37 @@
|
||||||
import { IUpdateInfo, updateElectronApp } from "update-electron-app";
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import { BrowserWindow, Notification, app, shell } from "electron";
|
import { BrowserWindow, app, shell } from "electron";
|
||||||
import started from "electron-squirrel-startup";
|
import started from "electron-squirrel-startup";
|
||||||
|
|
||||||
|
import { aviaVersion } from "../package.json";
|
||||||
|
|
||||||
import { autoLaunch } from "./native/autoLaunch";
|
import { autoLaunch } from "./native/autoLaunch";
|
||||||
|
import { setBadgeCount } from "./native/badges";
|
||||||
import { config } from "./native/config";
|
import { config } from "./native/config";
|
||||||
import { initDiscordRpc } from "./native/discordRpc";
|
import { initDiscordRpc } from "./native/discordRpc";
|
||||||
|
import { startGamePresenceMonitor } from "./native/gamePresence";
|
||||||
|
import { checkForUpdates } from "./native/updater";
|
||||||
import { initTray } from "./native/tray";
|
import { initTray } from "./native/tray";
|
||||||
import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
|
import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
|
||||||
|
|
||||||
import * as fs from "fs";
|
if (process.platform === "linux") {
|
||||||
import * as path from "path";
|
app.commandLine.appendSwitch("disable-gpu-sandbox");
|
||||||
|
app.commandLine.appendSwitch("no-zygote");
|
||||||
|
app.commandLine.appendSwitch("use-gl", "desktop");
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyAppName = () => {
|
||||||
|
try {
|
||||||
|
app.setName("Sanctum");
|
||||||
|
app.name = "Sanctum";
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
app.setAppUserModelId("cloud.mithraic.sanctum");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (started) {
|
if (started) {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
|
@ -22,53 +43,75 @@ if (!config.hardwareAcceleration) {
|
||||||
|
|
||||||
const acquiredLock = app.requestSingleInstanceLock();
|
const acquiredLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
const onNotifyUser = (_info: IUpdateInfo) => {
|
|
||||||
const notification = new Notification({
|
|
||||||
title: "Update Available",
|
|
||||||
body: "Restart the app to install the update.",
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
notification.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadInject = () => {
|
const loadInject = () => {
|
||||||
if (!mainWindow) return;
|
if (!mainWindow) return;
|
||||||
|
|
||||||
mainWindow.webContents.on("dom-ready", async () => {
|
const wc = mainWindow.webContents;
|
||||||
|
wc.removeAllListeners("dom-ready");
|
||||||
|
wc.once("dom-ready", async () => {
|
||||||
try {
|
try {
|
||||||
const injectPath = path.join(__dirname, "inject.js");
|
if (mainWindow.isDestroyed() || wc.isDestroyed()) return;
|
||||||
const injectCode = fs.readFileSync(injectPath, "utf8");
|
|
||||||
await mainWindow.webContents.executeJavaScript(injectCode, true);
|
|
||||||
|
|
||||||
const categoryPath = path.join(__dirname, "aviaclientcategory.js");
|
const builtInLocalPlugins = [
|
||||||
const categoryCode = fs.readFileSync(categoryPath, "utf8");
|
{
|
||||||
await mainWindow.webContents.executeJavaScript(categoryCode, true);
|
id: "sanctum-vcsounds",
|
||||||
|
name: "VCSounds",
|
||||||
|
code: fs.readFileSync(path.join(__dirname, "VCSounds.js"), "utf8"),
|
||||||
|
enabled: true,
|
||||||
|
locked: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const themesPath = path.join(__dirname, "themes.js");
|
await wc.executeJavaScript(
|
||||||
const themesCode = fs.readFileSync(themesPath, "utf8");
|
`window.__SANCTUM_BUILTIN_LOCAL_PLUGINS__ = ${JSON.stringify(
|
||||||
await mainWindow.webContents.executeJavaScript(themesCode, true);
|
builtInLocalPlugins,
|
||||||
|
)};`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const favPath = path.join(__dirname, "aviafavsystem.js");
|
if (mainWindow.isDestroyed() || wc.isDestroyed()) return;
|
||||||
const favCode = fs.readFileSync(favPath, "utf8");
|
|
||||||
await mainWindow.webContents.executeJavaScript(favCode, true);
|
|
||||||
|
|
||||||
const pluginPath = path.join(__dirname, "pluginsupport.js");
|
const plugins: string[] = [
|
||||||
const pluginCode = fs.readFileSync(pluginPath, "utf8");
|
"inject.js",
|
||||||
await mainWindow.webContents.executeJavaScript(pluginCode, true);
|
"LocalPlugins.js",
|
||||||
|
"aviaclientcategory.js",
|
||||||
|
"themes.js",
|
||||||
|
"aviafavsystem.js",
|
||||||
|
"pluginsupport.js",
|
||||||
|
"aviaversion.js",
|
||||||
|
"repofrontend.js",
|
||||||
|
"ButtonFix.js",
|
||||||
|
"aviadesktopversion.js",
|
||||||
|
"customFrameNativeMenu.js",
|
||||||
|
"disableTrayIcon.js",
|
||||||
|
"gamePresenceSettings.js",
|
||||||
|
"clientBackup.js",
|
||||||
|
"LoginWithToken.js",
|
||||||
|
];
|
||||||
|
|
||||||
const badgePath = path.join(__dirname, "aviaversion.js");
|
for (const plugin of plugins) {
|
||||||
const badgeCode = fs.readFileSync(badgePath, "utf8");
|
if (mainWindow.isDestroyed() || wc.isDestroyed()) return;
|
||||||
await mainWindow.webContents.executeJavaScript(badgeCode, true);
|
const pluginPath: string = path.join(__dirname, plugin);
|
||||||
} catch {}
|
const pluginCode: string = fs.readFileSync(pluginPath, "utf8");
|
||||||
|
await wc.executeJavaScript(pluginCode, true);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (acquiredLock) {
|
if (acquiredLock) {
|
||||||
updateElectronApp({ onNotifyUser });
|
app.whenReady().then(() => {
|
||||||
|
applyAppName();
|
||||||
app.on("ready", () => {
|
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setTitle("Sanctum");
|
||||||
|
mainWindow.on("page-title-updated", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mainWindow.setTitle("Sanctum");
|
||||||
|
});
|
||||||
|
}
|
||||||
loadInject();
|
loadInject();
|
||||||
|
|
||||||
if (config.firstLaunch) {
|
if (config.firstLaunch) {
|
||||||
|
|
@ -80,9 +123,18 @@ if (acquiredLock) {
|
||||||
|
|
||||||
initTray();
|
initTray();
|
||||||
initDiscordRpc();
|
initDiscordRpc();
|
||||||
|
startGamePresenceMonitor();
|
||||||
|
checkForUpdates();
|
||||||
|
setBadgeCount(0);
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
app.setAppUserModelId("chat.stoat.notifications");
|
app.setAppUserModelId("cloud.mithraic.sanctum");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
app.setAboutPanelOptions({
|
||||||
|
version: aviaVersion,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,6 +153,13 @@ if (acquiredLock) {
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setTitle("Sanctum");
|
||||||
|
mainWindow.on("page-title-updated", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mainWindow.setTitle("Sanctum");
|
||||||
|
});
|
||||||
|
}
|
||||||
loadInject();
|
loadInject();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
|
|
|
||||||
61
src/native/about.ts
Normal file
61
src/native/about.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
import { mainWindow } from "./window";
|
||||||
|
|
||||||
|
// global reference to about window
|
||||||
|
export let aboutWindow: BrowserWindow;
|
||||||
|
|
||||||
|
// Create our about window
|
||||||
|
export function createAboutWindow() {
|
||||||
|
// If our about window already exists, show it
|
||||||
|
if (aboutWindow) {
|
||||||
|
aboutWindow.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutWindow = new BrowserWindow({
|
||||||
|
minWidth: 300,
|
||||||
|
minHeight: 300,
|
||||||
|
width: 1024,
|
||||||
|
height: 720,
|
||||||
|
center: true,
|
||||||
|
backgroundColor: "#191919",
|
||||||
|
frame: true,
|
||||||
|
resizable: false,
|
||||||
|
minimizable: false,
|
||||||
|
parent: mainWindow,
|
||||||
|
paintWhenInitiallyHidden: true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, "preload.js"),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
spellcheck: false,
|
||||||
|
devTools: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove the default menu
|
||||||
|
aboutWindow.setMenu(null);
|
||||||
|
|
||||||
|
aboutWindow.loadFile(join(__dirname, "about.html"));
|
||||||
|
|
||||||
|
aboutWindow.on("ready-to-show", () => {
|
||||||
|
aboutWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
aboutWindow.on("closed", () => {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
aboutWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close window on Escape
|
||||||
|
aboutWindow.webContents.on("before-input-event", (event, input) => {
|
||||||
|
if (input.key.toLowerCase() === "escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
aboutWindow.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,6 @@ import AutoLaunch from "auto-launch";
|
||||||
|
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
import { mainWindow } from "./window";
|
|
||||||
|
|
||||||
export const autoLaunch = new AutoLaunch({
|
export const autoLaunch = new AutoLaunch({
|
||||||
name: "Stoat",
|
name: "Stoat",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,15 @@ const schema = {
|
||||||
customFrame: {
|
customFrame: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
} as JSONSchema.Boolean,
|
} as JSONSchema.Boolean,
|
||||||
|
customFrameNativeMenu: {
|
||||||
|
type: "boolean",
|
||||||
|
} as JSONSchema.Boolean,
|
||||||
minimiseToTray: {
|
minimiseToTray: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
} as JSONSchema.Boolean,
|
} as JSONSchema.Boolean,
|
||||||
|
disableTrayClick: {
|
||||||
|
type: "boolean",
|
||||||
|
} as JSONSchema.Boolean,
|
||||||
startMinimisedToTray: {
|
startMinimisedToTray: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
} as JSONSchema.Boolean,
|
} as JSONSchema.Boolean,
|
||||||
|
|
@ -28,6 +34,15 @@ const schema = {
|
||||||
discordRpc: {
|
discordRpc: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
} as JSONSchema.Boolean,
|
} as JSONSchema.Boolean,
|
||||||
|
gamePresenceEnabled: {
|
||||||
|
type: "boolean",
|
||||||
|
} as JSONSchema.Boolean,
|
||||||
|
gamePresenceRestrictToAllowList: {
|
||||||
|
type: "boolean",
|
||||||
|
} as JSONSchema.Boolean,
|
||||||
|
gamePresenceAllowList: {
|
||||||
|
type: "string",
|
||||||
|
} as JSONSchema.String,
|
||||||
windowState: {
|
windowState: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -55,11 +70,16 @@ const store = new Store({
|
||||||
defaults: {
|
defaults: {
|
||||||
firstLaunch: true,
|
firstLaunch: true,
|
||||||
customFrame: true,
|
customFrame: true,
|
||||||
|
customFrameNativeMenu: false,
|
||||||
minimiseToTray: true,
|
minimiseToTray: true,
|
||||||
|
disableTrayClick: false,
|
||||||
startMinimisedToTray: false,
|
startMinimisedToTray: false,
|
||||||
spellchecker: true,
|
spellchecker: true,
|
||||||
hardwareAcceleration: true,
|
hardwareAcceleration: true,
|
||||||
discordRpc: true,
|
discordRpc: true,
|
||||||
|
gamePresenceEnabled: true,
|
||||||
|
gamePresenceRestrictToAllowList: true,
|
||||||
|
gamePresenceAllowList: "",
|
||||||
windowState: {
|
windowState: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
|
@ -78,11 +98,16 @@ class Config {
|
||||||
mainWindow.webContents.send("config", {
|
mainWindow.webContents.send("config", {
|
||||||
firstLaunch: this.firstLaunch,
|
firstLaunch: this.firstLaunch,
|
||||||
customFrame: this.customFrame,
|
customFrame: this.customFrame,
|
||||||
|
customFrameNativeMenu: this.customFrameNativeMenu,
|
||||||
minimiseToTray: this.minimiseToTray,
|
minimiseToTray: this.minimiseToTray,
|
||||||
|
disableTrayClick: this.disableTrayClick,
|
||||||
startMinimisedToTray: this.startMinimisedToTray,
|
startMinimisedToTray: this.startMinimisedToTray,
|
||||||
spellchecker: this.spellchecker,
|
spellchecker: this.spellchecker,
|
||||||
hardwareAcceleration: this.hardwareAcceleration,
|
hardwareAcceleration: this.hardwareAcceleration,
|
||||||
discordRpc: this.discordRpc,
|
discordRpc: this.discordRpc,
|
||||||
|
gamePresenceEnabled: this.gamePresenceEnabled,
|
||||||
|
gamePresenceRestrictToAllowList: this.gamePresenceRestrictToAllowList,
|
||||||
|
gamePresenceAllowList: this.gamePresenceAllowList,
|
||||||
windowState: this.windowState,
|
windowState: this.windowState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +138,34 @@ class Config {
|
||||||
this.sync();
|
this.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get customFrameNativeMenu() {
|
||||||
|
return (store as never as { get(k: string): boolean }).get("customFrameNativeMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
set customFrameNativeMenu(value: boolean) {
|
||||||
|
(store as never as { set(k: string, value: boolean): void }).set(
|
||||||
|
"customFrameNativeMenu",
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
get disableTrayClick() {
|
||||||
|
return (store as never as { get(k: string): boolean }).get(
|
||||||
|
"disableTrayClick",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set disableTrayClick(value: boolean) {
|
||||||
|
(store as never as { set(k: string, value: boolean): void }).set(
|
||||||
|
"disableTrayClick",
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sync();
|
||||||
|
}
|
||||||
|
|
||||||
get minimiseToTray() {
|
get minimiseToTray() {
|
||||||
return (store as never as { get(k: string): boolean }).get(
|
return (store as never as { get(k: string): boolean }).get(
|
||||||
"minimiseToTray",
|
"minimiseToTray",
|
||||||
|
|
@ -192,6 +245,47 @@ class Config {
|
||||||
this.sync();
|
this.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get gamePresenceEnabled() {
|
||||||
|
return (store as never as { get(k: string): boolean }).get("gamePresenceEnabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
set gamePresenceEnabled(value: boolean) {
|
||||||
|
(store as never as { set(k: string, value: boolean): void }).set(
|
||||||
|
"gamePresenceEnabled",
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
get gamePresenceRestrictToAllowList() {
|
||||||
|
return (store as never as { get(k: string): boolean }).get(
|
||||||
|
"gamePresenceRestrictToAllowList",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set gamePresenceRestrictToAllowList(value: boolean) {
|
||||||
|
(store as never as { set(k: string, value: boolean): void }).set(
|
||||||
|
"gamePresenceRestrictToAllowList",
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
get gamePresenceAllowList() {
|
||||||
|
return (store as never as { get(k: string): string }).get("gamePresenceAllowList");
|
||||||
|
}
|
||||||
|
|
||||||
|
set gamePresenceAllowList(value: string) {
|
||||||
|
(store as never as { set(k: string, value: string): void }).set(
|
||||||
|
"gamePresenceAllowList",
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sync();
|
||||||
|
}
|
||||||
|
|
||||||
get windowState() {
|
get windowState() {
|
||||||
return (
|
return (
|
||||||
store as never as { get(k: string): DesktopConfig["windowState"] }
|
store as never as { get(k: string): DesktopConfig["windowState"] }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,32 @@ import { Client } from "discord-rpc";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
// internal state
|
// internal state
|
||||||
let rpc: Client;
|
let rpc: Client | undefined;
|
||||||
|
type RpcActivity = Parameters<Client["setActivity"]>[0];
|
||||||
|
|
||||||
|
const defaultActivity: RpcActivity = {
|
||||||
|
details: "Chatting with others on Sanctum",
|
||||||
|
state: "stoat.chat",
|
||||||
|
largeImageKey: "qr",
|
||||||
|
largeImageText: "Join Stoat!",
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: "Join Stoat",
|
||||||
|
url: "https://stoat.chat/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let pendingActivity: RpcActivity = defaultActivity;
|
||||||
|
|
||||||
|
function applyActivity() {
|
||||||
|
if (!rpc) return;
|
||||||
|
try {
|
||||||
|
rpc.setActivity(pendingActivity);
|
||||||
|
} catch {
|
||||||
|
/* ignore transient RPC failures */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function initDiscordRpc() {
|
export async function initDiscordRpc() {
|
||||||
if (!config.discordRpc) return;
|
if (!config.discordRpc) return;
|
||||||
|
|
@ -14,31 +39,24 @@ export async function initDiscordRpc() {
|
||||||
try {
|
try {
|
||||||
rpc = new Client({ transport: "ipc" });
|
rpc = new Client({ transport: "ipc" });
|
||||||
|
|
||||||
rpc.on("ready", () =>
|
rpc.on("ready", applyActivity);
|
||||||
rpc.setActivity({
|
|
||||||
state: "stoat.chat",
|
|
||||||
details: "Chatting with others",
|
|
||||||
largeImageKey: "qr",
|
|
||||||
largeImageText: "Join Stoat!",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
label: "Join Stoat",
|
|
||||||
url: "https://stoat.chat/",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
rpc.on("disconnected", reconnect);
|
rpc.on("disconnected", reconnect);
|
||||||
|
|
||||||
rpc.login({ clientId: "872068124005007420" });
|
rpc.login({ clientId: "1490783938829090837" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reconnect();
|
reconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setDiscordActivity(activity: RpcActivity | null) {
|
||||||
|
pendingActivity = activity ?? defaultActivity;
|
||||||
|
applyActivity();
|
||||||
|
}
|
||||||
|
|
||||||
const reconnect = () => setTimeout(() => initDiscordRpc(), 1e4);
|
const reconnect = () => setTimeout(() => initDiscordRpc(), 1e4);
|
||||||
|
|
||||||
export async function destroyDiscordRpc() {
|
export async function destroyDiscordRpc() {
|
||||||
rpc?.destroy();
|
rpc?.destroy();
|
||||||
|
rpc = undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
173
src/native/gameCatalog.ts
Normal file
173
src/native/gameCatalog.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
type CandidateLike = {
|
||||||
|
processName: string;
|
||||||
|
title: string;
|
||||||
|
commandLine?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GameCatalogEntry = {
|
||||||
|
name: string;
|
||||||
|
aliases?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const GAME_CATALOG: GameCatalogEntry[] = [
|
||||||
|
{ name: "Apex Legends", aliases: ["apex", "r5apex"] },
|
||||||
|
{ name: "Among Us" },
|
||||||
|
{ name: "Assassin's Creed Mirage" },
|
||||||
|
{ name: "Assassin's Creed Valhalla" },
|
||||||
|
{ name: "Armored Core VI: Fires of Rubicon", aliases: ["armored core 6"] },
|
||||||
|
{ name: "Baldur's Gate 3", aliases: ["bg3", "baldurs gate 3", "baldursgate3"] },
|
||||||
|
{ name: "Black Myth: Wukong", aliases: ["blackmythwukong", "wukong"] },
|
||||||
|
{ name: "Brawlhalla" },
|
||||||
|
{ name: "Call of Duty: Black Ops 6", aliases: ["black ops 6", "codbo6"] },
|
||||||
|
{ name: "Call of Duty: Modern Warfare III", aliases: ["modern warfare 3", "mw3", "codmw3"] },
|
||||||
|
{ name: "Call of Duty: Warzone", aliases: ["warzone", "cod warzone"] },
|
||||||
|
{ name: "Celeste" },
|
||||||
|
{ name: "Cities: Skylines II", aliases: ["cities skylines 2", "skylines 2"] },
|
||||||
|
{ name: "Civilization VI", aliases: ["civ6", "civilization 6"] },
|
||||||
|
{ name: "Counter-Strike 2", aliases: ["cs2", "counter strike 2", "csgo", "counter strike global offensive"] },
|
||||||
|
{ name: "Cuphead" },
|
||||||
|
{ name: "Cyberpunk 2077", aliases: ["cyberpunk"] },
|
||||||
|
{ name: "Dark Souls III", aliases: ["dark souls 3"] },
|
||||||
|
{ name: "Dave the Diver" },
|
||||||
|
{ name: "Days Gone" },
|
||||||
|
{ name: "Dead by Daylight" },
|
||||||
|
{ name: "Dead Cells" },
|
||||||
|
{ name: "Deep Rock Galactic" },
|
||||||
|
{ name: "Destiny 2" },
|
||||||
|
{ name: "Diablo IV", aliases: ["diablo 4"] },
|
||||||
|
{ name: "Dota 2" },
|
||||||
|
{ name: "Dragon's Dogma 2", aliases: ["dragons dogma 2"] },
|
||||||
|
{ name: "Elden Ring" },
|
||||||
|
{ name: "Enshrouded" },
|
||||||
|
{ name: "Escape from Tarkov" },
|
||||||
|
{ name: "Euro Truck Simulator 2" },
|
||||||
|
{ name: "EVE Online" },
|
||||||
|
{ name: "Fall Guys" },
|
||||||
|
{ name: "Fallout 4" },
|
||||||
|
{ name: "Fallout 76" },
|
||||||
|
{ name: "Factorio" },
|
||||||
|
{ name: "F1 24" },
|
||||||
|
{ name: "Final Fantasy XIV", aliases: ["ffxiv"] },
|
||||||
|
{ name: "Forza Horizon 5" },
|
||||||
|
{ name: "Fortnite", aliases: ["fortniteclient", "fortniteclientwin64shipping"] },
|
||||||
|
{ name: "Genshin Impact", aliases: ["genshin", "genshinimpact", "yuanshen"] },
|
||||||
|
{ name: "Ghost of Tsushima" },
|
||||||
|
{ name: "God of War" },
|
||||||
|
{ name: "Grand Theft Auto V", aliases: ["gta5", "gta v"] },
|
||||||
|
{ name: "Grounded" },
|
||||||
|
{ name: "Guild Wars 2" },
|
||||||
|
{ name: "Hades" },
|
||||||
|
{ name: "Hades II" },
|
||||||
|
{ name: "Helldivers 2" },
|
||||||
|
{ name: "Hogwarts Legacy" },
|
||||||
|
{ name: "Hollow Knight" },
|
||||||
|
{ name: "Honkai: Star Rail", aliases: ["hkrpg", "hsr", "star rail"] },
|
||||||
|
{ name: "Honkai Impact 3rd" },
|
||||||
|
{ name: "Hunt: Showdown" },
|
||||||
|
{ name: "It Takes Two" },
|
||||||
|
{ name: "Kingdom Come: Deliverance" },
|
||||||
|
{ name: "League of Legends", aliases: ["leagueclient", "league of legends", "lolclient"] },
|
||||||
|
{ name: "Lethal Company" },
|
||||||
|
{ name: "Left 4 Dead 2" },
|
||||||
|
{ name: "Last Epoch" },
|
||||||
|
{ name: "Marvel Rivals" },
|
||||||
|
{ name: "Minecraft", aliases: ["minecraftlauncher", "minecraft java edition", "javaw"] },
|
||||||
|
{ name: "Monster Hunter: World", aliases: ["monster hunter world", "mhw"] },
|
||||||
|
{ name: "Monster Hunter Rise", aliases: ["monster hunter rise", "mhr"] },
|
||||||
|
{ name: "Mortal Kombat 1", aliases: ["mk1"] },
|
||||||
|
{ name: "Metaphor: ReFantazio" },
|
||||||
|
{ name: "No Man's Sky" },
|
||||||
|
{ name: "Once Human" },
|
||||||
|
{ name: "Overwatch 2", aliases: ["overwatch", "ow2"] },
|
||||||
|
{ name: "Palworld" },
|
||||||
|
{ name: "Path of Exile", aliases: ["poe", "pathofexile"] },
|
||||||
|
{ name: "Path of Exile 2", aliases: ["poe2", "pathofexile2"] },
|
||||||
|
{ name: "Persona 5 Royal" },
|
||||||
|
{ name: "Phasmophobia" },
|
||||||
|
{ name: "PUBG: Battlegrounds", aliases: ["pubg"] },
|
||||||
|
{ name: "Paladins" },
|
||||||
|
{ name: "Rainbow Six Siege", aliases: ["r6 siege", "r6siege", "siege"] },
|
||||||
|
{ name: "Red Dead Redemption 2", aliases: ["rdr2"] },
|
||||||
|
{ name: "Resident Evil 4", aliases: ["re4 remake", "resident evil 4 remake"] },
|
||||||
|
{ name: "Resident Evil Village", aliases: ["re8", "resident evil 8"] },
|
||||||
|
{ name: "Rocket League", aliases: ["rocketleague"] },
|
||||||
|
{ name: "Rust" },
|
||||||
|
{ name: "Satisfactory" },
|
||||||
|
{ name: "Sea of Thieves", aliases: ["seaofthieves"] },
|
||||||
|
{ name: "Skyrim Special Edition", aliases: ["skyrimse", "tesv special edition"] },
|
||||||
|
{ name: "Slay the Spire" },
|
||||||
|
{ name: "Sons of the Forest" },
|
||||||
|
{ name: "Spider-Man Remastered", aliases: ["spidermanremastered", "marvel spiderman remastered"] },
|
||||||
|
{ name: "Split Fiction" },
|
||||||
|
{ name: "Star Citizen" },
|
||||||
|
{ name: "Starfield" },
|
||||||
|
{ name: "Stardew Valley" },
|
||||||
|
{ name: "Street Fighter 6", aliases: ["sf6"] },
|
||||||
|
{ name: "Subnautica" },
|
||||||
|
{ name: "Team Fortress 2", aliases: ["tf2"] },
|
||||||
|
{ name: "Tekken 8", aliases: ["tekken8"] },
|
||||||
|
{ name: "Terraria" },
|
||||||
|
{ name: "The Elder Scrolls Online", aliases: ["eso"] },
|
||||||
|
{ name: "The Finals" },
|
||||||
|
{ name: "The Last of Us Part I", aliases: ["the last of us", "tlou"] },
|
||||||
|
{ name: "The Witcher 3", aliases: ["witcher 3", "witcher3"] },
|
||||||
|
{ name: "Titanfall 2" },
|
||||||
|
{ name: "VALORANT", aliases: ["valorant-win64-shipping", "valorant-win64", "valorant"] },
|
||||||
|
{ name: "V Rising" },
|
||||||
|
{ name: "Valheim" },
|
||||||
|
{ name: "Warframe" },
|
||||||
|
{ name: "War Thunder" },
|
||||||
|
{ name: "Wuthering Waves", aliases: ["wutheringwaves", "wuwa"] },
|
||||||
|
{ name: "World of Warcraft", aliases: ["wow", "wowclassic", "worldofwarcraft"] },
|
||||||
|
{ name: "World of Tanks", aliases: ["wot"] },
|
||||||
|
{ name: "World of Warships", aliases: ["wowships"] },
|
||||||
|
{ name: "Zenless Zone Zero", aliases: ["zzz"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const GAME_MATCHERS = GAME_CATALOG.flatMap((entry) =>
|
||||||
|
[entry.name, ...(entry.aliases || [])].flatMap((value) => buildNeedles(value)),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function parseGameAllowList(raw: string) {
|
||||||
|
return String(raw || "")
|
||||||
|
.split(/[\n,]+/)
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeGameText(value: string) {
|
||||||
|
return String(value || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\.(exe|app|bat|sh)$/g, "")
|
||||||
|
.replace(/[^a-z0-9]+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesKnownGame(candidate: CandidateLike, allowListRaw: string) {
|
||||||
|
const haystack = normalizeGameText(
|
||||||
|
`${candidate.processName} ${candidate.title} ${candidate.commandLine || ""}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!haystack) return false;
|
||||||
|
if (GAME_MATCHERS.some((matcher) => haystack.includes(matcher))) return true;
|
||||||
|
|
||||||
|
return parseGameAllowList(allowListRaw).some((item) =>
|
||||||
|
haystack.includes(normalizeGameText(item)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNeedles(value: string) {
|
||||||
|
const raw = String(value || "").trim();
|
||||||
|
if (!raw) return [];
|
||||||
|
|
||||||
|
const collapsed = raw.replace(/[’']/g, "");
|
||||||
|
const variants = [
|
||||||
|
raw,
|
||||||
|
raw.toLowerCase(),
|
||||||
|
collapsed,
|
||||||
|
collapsed.toLowerCase(),
|
||||||
|
normalizeGameText(raw),
|
||||||
|
normalizeGameText(collapsed),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Array.from(new Set(variants.map((item) => item.trim()).filter(Boolean)));
|
||||||
|
}
|
||||||
373
src/native/gameOverlay.ts
Normal file
373
src/native/gameOverlay.ts
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
import { BrowserWindow, ipcMain, screen } from "electron";
|
||||||
|
|
||||||
|
import { config } from "./config";
|
||||||
|
import { mainWindow } from "./window";
|
||||||
|
|
||||||
|
type GamePresence = {
|
||||||
|
title: string;
|
||||||
|
processName: string;
|
||||||
|
startedAt: number;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OverlayState = {
|
||||||
|
game: GamePresence | null;
|
||||||
|
voice: VoiceOverlayState | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let overlayWindow: BrowserWindow | null = null;
|
||||||
|
let currentState: OverlayState = {
|
||||||
|
game: null,
|
||||||
|
voice: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function publishActivityState() {
|
||||||
|
const state = currentState;
|
||||||
|
mainWindow?.webContents.send("sanctum-activity:update", state);
|
||||||
|
overlayWindow?.webContents.send("sanctum-activity:update", state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HTML = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: rgba(18, 20, 28, 0.88);
|
||||||
|
--border: rgba(255, 255, 255, 0.08);
|
||||||
|
--text: rgba(255, 255, 255, 0.96);
|
||||||
|
--muted: rgba(255, 255, 255, 0.58);
|
||||||
|
--accent: #8fb2ff;
|
||||||
|
--speaking: #59f2a3;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.shell {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 160ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
.shell.is-flashing {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.voice {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.members {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.member {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255,255,255,0.94);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
position: relative;
|
||||||
|
text-transform: uppercase;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
opacity: 0.22;
|
||||||
|
transition: opacity 90ms linear;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
object-fit: cover;
|
||||||
|
clip-path: circle(50% at 50% 50%);
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.member.speaking {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.member.self.speaking {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.member .initials {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-shadow: 0 1px 1px rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
|
.member.has-avatar {
|
||||||
|
color: transparent;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
.member.has-avatar .initials {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<div class="members" id="members"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const state = { game: null, voice: null };
|
||||||
|
const membersEl = document.getElementById("members");
|
||||||
|
const shellEl = document.querySelector(".shell");
|
||||||
|
let previousVoiceSignature = "";
|
||||||
|
let flashTimeout = null;
|
||||||
|
|
||||||
|
function getInitials(name) {
|
||||||
|
const value = String(name || "").trim();
|
||||||
|
if (!value) return "?";
|
||||||
|
const parts = value.split(/\s+/).filter(Boolean);
|
||||||
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||||
|
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpeakingMember(member) {
|
||||||
|
return Boolean(member?.speaking);
|
||||||
|
}
|
||||||
|
|
||||||
|
function voiceSignature(voice) {
|
||||||
|
if (!voice || !voice.members) return "";
|
||||||
|
return voice.members
|
||||||
|
.map((member) => [
|
||||||
|
String(member?.name || "").trim().toLowerCase(),
|
||||||
|
String(member?.avatarUrl || "").trim(),
|
||||||
|
].join(":"))
|
||||||
|
.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function flashShell() {
|
||||||
|
if (!shellEl) return;
|
||||||
|
shellEl.classList.add("is-flashing");
|
||||||
|
clearTimeout(flashTimeout);
|
||||||
|
flashTimeout = setTimeout(() => {
|
||||||
|
shellEl.classList.remove("is-flashing");
|
||||||
|
}, 1400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const voice = state.voice;
|
||||||
|
const hasSpeaking = Boolean(voice?.members?.some((member) => member?.speaking));
|
||||||
|
membersEl.innerHTML = "";
|
||||||
|
|
||||||
|
if (!voice || !voice.members || !voice.members.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const member of voice.members.slice(0, 5)) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "member" + (isSpeakingMember(member) ? " speaking" : "") + (member.name === "You" ? " self" : "");
|
||||||
|
row.title = member.name || "Unknown";
|
||||||
|
const initials = document.createElement("span");
|
||||||
|
initials.className = "initials";
|
||||||
|
initials.textContent = getInitials(member.name);
|
||||||
|
if (member.avatarUrl) {
|
||||||
|
row.classList.add("has-avatar");
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.className = "avatar";
|
||||||
|
img.alt = member.name || "Avatar";
|
||||||
|
img.draggable = false;
|
||||||
|
img.src = String(member.avatarUrl);
|
||||||
|
row.appendChild(img);
|
||||||
|
} else {
|
||||||
|
row.classList.remove("has-avatar");
|
||||||
|
}
|
||||||
|
row.appendChild(initials);
|
||||||
|
membersEl.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.on("overlay-state", (_, next) => {
|
||||||
|
state.game = next?.game || null;
|
||||||
|
state.voice = next?.voice || null;
|
||||||
|
const nextSignature = voiceSignature(state.voice);
|
||||||
|
if (nextSignature && nextSignature !== previousVoiceSignature) {
|
||||||
|
flashShell();
|
||||||
|
}
|
||||||
|
previousVoiceSignature = nextSignature;
|
||||||
|
if (!state.voice || !state.voice.members || !state.voice.members.length) {
|
||||||
|
flashShell();
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
function getOverlayBounds() {
|
||||||
|
const display = screen.getPrimaryDisplay();
|
||||||
|
return { ...display.workArea };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureOverlayWindow() {
|
||||||
|
if (overlayWindow) return overlayWindow;
|
||||||
|
|
||||||
|
const bounds = getOverlayBounds();
|
||||||
|
|
||||||
|
overlayWindow = new BrowserWindow({
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
frame: false,
|
||||||
|
transparent: true,
|
||||||
|
resizable: false,
|
||||||
|
movable: false,
|
||||||
|
minimizable: false,
|
||||||
|
maximizable: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
focusable: false,
|
||||||
|
show: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
hasShadow: false,
|
||||||
|
backgroundColor: "#00000000",
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
overlayWindow.setMenu(null);
|
||||||
|
overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
overlayWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
overlayWindow.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(HTML));
|
||||||
|
overlayWindow.webContents.on("did-finish-load", () => {
|
||||||
|
syncOverlayWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
overlayWindow.on("closed", () => {
|
||||||
|
overlayWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return overlayWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowOverlay() {
|
||||||
|
if (!config.gamePresenceEnabled) return false;
|
||||||
|
return Boolean(
|
||||||
|
currentState.game &&
|
||||||
|
currentState.voice &&
|
||||||
|
currentState.voice.isInCall,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOverlayWindow() {
|
||||||
|
if (!overlayWindow) return;
|
||||||
|
|
||||||
|
if (!shouldShowOverlay()) {
|
||||||
|
if (overlayWindow.isVisible()) overlayWindow.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayWindow.showInactive();
|
||||||
|
overlayWindow.webContents.send("overlay-state", currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGamePresence(game: GamePresence | null) {
|
||||||
|
if (!config.gamePresenceEnabled) {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
game: null,
|
||||||
|
};
|
||||||
|
syncOverlayWindow();
|
||||||
|
publishActivityState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game && /^(sanctum|stoat|electron)$/i.test(game.processName)) {
|
||||||
|
game = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
game,
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureOverlayWindow();
|
||||||
|
syncOverlayWindow();
|
||||||
|
publishActivityState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setVoiceOverlayState(voice: VoiceOverlayState | null) {
|
||||||
|
if (!config.gamePresenceEnabled) {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
voice,
|
||||||
|
};
|
||||||
|
publishActivityState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
voice,
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureOverlayWindow();
|
||||||
|
syncOverlayWindow();
|
||||||
|
publishActivityState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debugSetActivityState(state: OverlayState) {
|
||||||
|
currentState = {
|
||||||
|
game: state.game || null,
|
||||||
|
voice: state.voice || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureOverlayWindow();
|
||||||
|
syncOverlayWindow();
|
||||||
|
publishActivityState();
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on("overlay:set-voice-state", (_event, state: VoiceOverlayState | null) => {
|
||||||
|
setVoiceOverlayState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("sanctum-activity:get-state", () => currentState);
|
||||||
|
ipcMain.handle("sanctum-activity:debug-set-state", (_event, state: OverlayState) => {
|
||||||
|
debugSetActivityState(state);
|
||||||
|
return currentState;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getCurrentGamePresence() {
|
||||||
|
return currentState.game;
|
||||||
|
}
|
||||||
335
src/native/gamePresence.ts
Normal file
335
src/native/gamePresence.ts
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
import { config } from "./config";
|
||||||
|
import { matchesKnownGame } from "./gameCatalog";
|
||||||
|
import { getCurrentGamePresence, setGamePresence } from "./gameOverlay";
|
||||||
|
|
||||||
|
type Candidate = {
|
||||||
|
title: string;
|
||||||
|
processName: string;
|
||||||
|
source: string;
|
||||||
|
commandLine?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
let monitorTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const IGNORE_PATTERNS = [
|
||||||
|
/^sanctum$/i,
|
||||||
|
/^cloud\.mithraic\.sanctum$/i,
|
||||||
|
/^mithral$/i,
|
||||||
|
/^stoat$/i,
|
||||||
|
/^electron$/i,
|
||||||
|
/^chrome$/i,
|
||||||
|
/^google chrome$/i,
|
||||||
|
/^msedge$/i,
|
||||||
|
/^microsoft edge$/i,
|
||||||
|
/^firefox$/i,
|
||||||
|
/^brave$/i,
|
||||||
|
/^brave browser$/i,
|
||||||
|
/^vivaldi$/i,
|
||||||
|
/^opera$/i,
|
||||||
|
/^opera gx$/i,
|
||||||
|
/^arc$/i,
|
||||||
|
/^safari$/i,
|
||||||
|
/^finder$/i,
|
||||||
|
/^launchpad$/i,
|
||||||
|
/^terminal$/i,
|
||||||
|
/^iterm2$/i,
|
||||||
|
/^steam$/i,
|
||||||
|
/^steamwebhelper$/i,
|
||||||
|
/^discord$/i,
|
||||||
|
/^slack$/i,
|
||||||
|
/^teams$/i,
|
||||||
|
/^zoom$/i,
|
||||||
|
/^notion$/i,
|
||||||
|
/^obsidian$/i,
|
||||||
|
/^spotify$/i,
|
||||||
|
/^telegram$/i,
|
||||||
|
/^whatsapp$/i,
|
||||||
|
/^code$/i,
|
||||||
|
/^visual studio code$/i,
|
||||||
|
/^node$/i,
|
||||||
|
/^explorer$/i,
|
||||||
|
/^file explorer$/i,
|
||||||
|
/^system$/i,
|
||||||
|
/^systemsettings$/i,
|
||||||
|
/^settings$/i,
|
||||||
|
/^textedit$/i,
|
||||||
|
/^notes$/i,
|
||||||
|
/^preview$/i,
|
||||||
|
/^activity monitor$/i,
|
||||||
|
/^app store$/i,
|
||||||
|
/^messages$/i,
|
||||||
|
/^mail$/i,
|
||||||
|
/^outlook$/i,
|
||||||
|
/^word$/i,
|
||||||
|
/^excel$/i,
|
||||||
|
/^powerpoint$/i,
|
||||||
|
/^python$/i,
|
||||||
|
/^bash$/i,
|
||||||
|
/^zsh$/i,
|
||||||
|
/^sh$/i,
|
||||||
|
/^ps$/i,
|
||||||
|
/^tasklist$/i,
|
||||||
|
/^powershell$/i,
|
||||||
|
/^pwsh$/i,
|
||||||
|
/^xprop$/i,
|
||||||
|
/^xdotool$/i,
|
||||||
|
/^osascript$/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SELF_PATTERNS = [
|
||||||
|
/sanctum/i,
|
||||||
|
/stoat/i,
|
||||||
|
/cloud\.mithraic\.sanctum/i,
|
||||||
|
/mithraic\.space/i,
|
||||||
|
/stoat\.chat/i,
|
||||||
|
/electron-forge/i,
|
||||||
|
/\/home\/[^/]+\/sanctum/i,
|
||||||
|
/[A-Z]:\\.*\\sanctum/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function startGamePresenceMonitor() {
|
||||||
|
if (monitorTimer) return;
|
||||||
|
void refreshGamePresence();
|
||||||
|
monitorTimer = setInterval(() => {
|
||||||
|
void refreshGamePresence();
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshGamePresence() {
|
||||||
|
if (!config.gamePresenceEnabled) {
|
||||||
|
if (getCurrentGamePresence()) {
|
||||||
|
setGamePresence(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = await detectGameCandidate();
|
||||||
|
const current = getCurrentGamePresence();
|
||||||
|
const knownMatch = next ? matchesKnownGame(next, config.gamePresenceAllowList) : false;
|
||||||
|
const accepted = next && !isSelfAppCandidate(next) && knownMatch ? next : null;
|
||||||
|
|
||||||
|
const same =
|
||||||
|
(!!current &&
|
||||||
|
!!accepted &&
|
||||||
|
current.processName === accepted.processName &&
|
||||||
|
current.title === accepted.title) ||
|
||||||
|
(!current && !accepted);
|
||||||
|
|
||||||
|
if (same) return;
|
||||||
|
|
||||||
|
if (accepted) {
|
||||||
|
console.info("[gamePresence] detected", accepted.processName, accepted.title, accepted.source);
|
||||||
|
} else if (current) {
|
||||||
|
console.info("[gamePresence] cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
setGamePresence(accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectGameCandidate(): Promise<Candidate | null> {
|
||||||
|
try {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return await detectWindowsGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return await detectMacGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await detectUnixGame();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectWindowsGame(): Promise<Candidate | null> {
|
||||||
|
const script = [
|
||||||
|
"$sig=@'",
|
||||||
|
"using System;",
|
||||||
|
"using System.Text;",
|
||||||
|
"using System.Runtime.InteropServices;",
|
||||||
|
"public static class Win32 {",
|
||||||
|
" [DllImport(\"user32.dll\")] public static extern IntPtr GetForegroundWindow();",
|
||||||
|
" [DllImport(\"user32.dll\", SetLastError=true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);",
|
||||||
|
" [DllImport(\"user32.dll\", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);",
|
||||||
|
"}",
|
||||||
|
"'@;",
|
||||||
|
"Add-Type $sig | Out-Null;",
|
||||||
|
"$h=[Win32]::GetForegroundWindow();",
|
||||||
|
"$pid=0;",
|
||||||
|
"[void][Win32]::GetWindowThreadProcessId($h,[ref]$pid);",
|
||||||
|
"$p=Get-Process -Id $pid -ErrorAction SilentlyContinue;",
|
||||||
|
"$sb=New-Object System.Text.StringBuilder 512;",
|
||||||
|
"[void][Win32]::GetWindowText($h,$sb,$sb.Capacity);",
|
||||||
|
"if ($p) { Write-Output ($p.ProcessName + '|' + $sb.ToString()) }",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
const { stdout } = await execFileAsync("powershell.exe", [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
script,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const parsed = parseCandidateLine(stdout.trim(), "foreground-window");
|
||||||
|
return parsed && !isIgnoredCandidate(parsed.processName, parsed.title, parsed.commandLine || "")
|
||||||
|
? parsed
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectMacGame(): Promise<Candidate | null> {
|
||||||
|
const { stdout } = await execFileAsync("osascript", [
|
||||||
|
"-e",
|
||||||
|
'tell application "System Events" to get name of first process whose frontmost is true',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const processName = stdout.trim();
|
||||||
|
if (!processName || isIgnoredCandidate(processName, processName, "")) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
processName,
|
||||||
|
title: formatGameTitle(processName),
|
||||||
|
source: "macOS frontmost app",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectUnixGame(): Promise<Candidate | null> {
|
||||||
|
const xpropCandidate = await detectLinuxX11Game();
|
||||||
|
if (xpropCandidate) return xpropCandidate;
|
||||||
|
|
||||||
|
const { stdout } = await execFileAsync("sh", [
|
||||||
|
"-lc",
|
||||||
|
[
|
||||||
|
"if command -v xdotool >/dev/null 2>&1; then",
|
||||||
|
" title=$(xdotool getactivewindow getwindowname 2>/dev/null || true)",
|
||||||
|
" pid=$(xdotool getactivewindow getwindowpid 2>/dev/null || true)",
|
||||||
|
" if [ -n \"$pid\" ]; then",
|
||||||
|
" name=$(ps -p \"$pid\" -o comm= 2>/dev/null | head -n 1 | tr -d '\\n')",
|
||||||
|
" args=$(ps -p \"$pid\" -o args= 2>/dev/null | head -n 1 | tr -d '\\n')",
|
||||||
|
" printf '%s|%s|%s\\n' \"$name\" \"$title\" \"$args\"",
|
||||||
|
" exit 0",
|
||||||
|
" fi",
|
||||||
|
"fi",
|
||||||
|
"exit 0",
|
||||||
|
].join("\n"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const trimmed = stdout.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
const parsed = parseCandidateLine(trimmed, "foreground-window");
|
||||||
|
if (parsed && !isIgnoredCandidate(parsed.processName, parsed.title, parsed.commandLine || "")) return parsed;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectLinuxX11Game(): Promise<Candidate | null> {
|
||||||
|
try {
|
||||||
|
const { stdout: activeWindow } = await execFileAsync("xprop", [
|
||||||
|
"-root",
|
||||||
|
"_NET_ACTIVE_WINDOW",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const match = activeWindow.match(/0x[0-9a-fA-F]+/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const windowId = match[0];
|
||||||
|
const { stdout } = await execFileAsync("xprop", [
|
||||||
|
"-id",
|
||||||
|
windowId,
|
||||||
|
"WM_CLASS",
|
||||||
|
"WM_NAME",
|
||||||
|
"_NET_WM_NAME",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const parts = stdout
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const className = extractQuotedValue(parts.find((line) => line.startsWith("WM_CLASS")) || "");
|
||||||
|
const name =
|
||||||
|
extractQuotedValue(parts.find((line) => line.startsWith("_NET_WM_NAME")) || "") ||
|
||||||
|
extractQuotedValue(parts.find((line) => line.startsWith("WM_NAME")) || "");
|
||||||
|
|
||||||
|
const processName = (className || name || "unknown").trim();
|
||||||
|
const title = (name || className || "").trim();
|
||||||
|
|
||||||
|
if (!processName) return null;
|
||||||
|
if (isIgnoredCandidate(processName, title || processName, "")) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
processName,
|
||||||
|
title: title || formatGameTitle(processName),
|
||||||
|
source: "xprop foreground window",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractQuotedValue(line: string) {
|
||||||
|
const quoted = line.match(/"([^"]+)"/g);
|
||||||
|
if (!quoted || !quoted.length) return "";
|
||||||
|
return quoted.map((value) => value.replace(/^"|"$/g, "")).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCandidateLine(line: string, source: string): Candidate | null {
|
||||||
|
if (!line) return null;
|
||||||
|
|
||||||
|
const [processNameRaw, titleRaw = "", commandLineRaw = ""] = line.split("|");
|
||||||
|
const processName = (processNameRaw || "").trim();
|
||||||
|
const title =
|
||||||
|
source === "process scan"
|
||||||
|
? formatGameTitle(processName)
|
||||||
|
: (titleRaw || "").trim() || formatGameTitle(processName);
|
||||||
|
const commandLine = (commandLineRaw || "").trim();
|
||||||
|
|
||||||
|
if (!processName) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
processName,
|
||||||
|
source,
|
||||||
|
commandLine: commandLine || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIgnoredCandidate(processName: string, title: string, commandLine = "") {
|
||||||
|
if (isSelfString(processName) || isSelfString(title) || isSelfString(commandLine)) return true;
|
||||||
|
if (IGNORE_PATTERNS.some((pattern) => pattern.test(processName))) return true;
|
||||||
|
if (IGNORE_PATTERNS.some((pattern) => pattern.test(title))) return true;
|
||||||
|
if (commandLine && IGNORE_PATTERNS.some((pattern) => pattern.test(commandLine))) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelfAppCandidate(candidate: Candidate) {
|
||||||
|
return isSelfString(candidate.processName) || isSelfString(candidate.title) || isSelfString(candidate.commandLine || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelfString(value: string) {
|
||||||
|
const normalized = String(value || "");
|
||||||
|
return SELF_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGameTitle(raw: string) {
|
||||||
|
const cleaned = raw
|
||||||
|
.replace(/\.(exe|app|bat|sh)$/i, "")
|
||||||
|
.replace(/[_.-]+/g, " ")
|
||||||
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!cleaned) return raw || "Unknown Game";
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Menu, Tray, nativeImage } from "electron";
|
import { Menu, Tray, app, nativeImage } from "electron";
|
||||||
|
|
||||||
import trayIconAsset from "../../assets/desktop/icon.png?asset";
|
import trayIconAsset from "../../avia_assets/icon.png?asset";
|
||||||
import macOsTrayIconAsset from "../../assets/desktop/iconTemplate.png?asset";
|
import macOsTrayIconAsset from "../../avia_assets/iconTemplate.png?asset";
|
||||||
import { version } from "../../package.json";
|
import { aviaVersion, version } from "../../package.json";
|
||||||
|
|
||||||
|
import { createAboutWindow } from "./about";
|
||||||
|
import { config } from "./config";
|
||||||
import { mainWindow, quitApp } from "./window";
|
import { mainWindow, quitApp } from "./window";
|
||||||
|
|
||||||
// internal tray state
|
// internal tray state
|
||||||
|
|
@ -25,14 +27,18 @@ export function initTray() {
|
||||||
const trayIcon = createTrayIcon();
|
const trayIcon = createTrayIcon();
|
||||||
tray = new Tray(trayIcon);
|
tray = new Tray(trayIcon);
|
||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
tray.setToolTip("Stoat for Desktop");
|
tray.setToolTip("Sanctum for Desktop");
|
||||||
tray.setImage(trayIcon);
|
tray.setImage(trayIcon);
|
||||||
tray.on("click", () => {
|
tray.on("click", () => {
|
||||||
|
config.sync();
|
||||||
|
if (config.disableTrayClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mainWindow.isVisible()) {
|
if (mainWindow.isVisible()) {
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -40,18 +46,30 @@ export function initTray() {
|
||||||
export function updateTrayMenu() {
|
export function updateTrayMenu() {
|
||||||
tray.setContextMenu(
|
tray.setContextMenu(
|
||||||
Menu.buildFromTemplate([
|
Menu.buildFromTemplate([
|
||||||
{ label: "Stoat for Desktop", type: "normal", enabled: false },
|
{ label: "Sanctum for Desktop", type: "normal", enabled: false },
|
||||||
{
|
{
|
||||||
label: "Version",
|
label: "Versions",
|
||||||
type: "submenu",
|
type: "submenu",
|
||||||
submenu: Menu.buildFromTemplate([
|
submenu: Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: version,
|
label: `Stoat Desktop: ${version}`,
|
||||||
|
type: "normal",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Sanctum: ${aviaVersion}`,
|
||||||
type: "normal",
|
type: "normal",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "About",
|
||||||
|
type: "normal",
|
||||||
|
click() {
|
||||||
|
createAboutWindow();
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: mainWindow.isVisible() ? "Hide App" : "Show App",
|
label: mainWindow.isVisible() ? "Hide App" : "Show App",
|
||||||
|
|
@ -64,6 +82,15 @@ export function updateTrayMenu() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Restart App",
|
||||||
|
type: "normal",
|
||||||
|
click() {
|
||||||
|
app.relaunch();
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Quit App",
|
label: "Quit App",
|
||||||
type: "normal",
|
type: "normal",
|
||||||
|
|
|
||||||
83
src/native/update-window.ts
Normal file
83
src/native/update-window.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { BrowserWindow, app, ipcMain } from "electron";
|
||||||
|
|
||||||
|
let win: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
const HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:system-ui,sans-serif;background:#1e1e2e;color:#cdd6f4;
|
||||||
|
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||||
|
height:100vh;padding:28px 32px;gap:14px;user-select:none;-webkit-app-region:drag}
|
||||||
|
h2{font-size:15px;font-weight:600;letter-spacing:.3px}
|
||||||
|
#status{font-size:13px;color:#a6adc8}
|
||||||
|
.track{width:100%;height:6px;background:#313244;border-radius:3px}
|
||||||
|
#bar{height:6px;background:#89b4fa;border-radius:3px;width:0%;transition:width .4s ease}
|
||||||
|
#btn{display:none;margin-top:6px;padding:9px 24px;background:#89b4fa;color:#1e1e2e;
|
||||||
|
border:none;border-radius:6px;font-size:13px;font-weight:700;cursor:pointer;
|
||||||
|
-webkit-app-region:no-drag}
|
||||||
|
#btn:hover{background:#b4befe}
|
||||||
|
</style></head><body>
|
||||||
|
<h2 id="title">Updating Sanctum</h2>
|
||||||
|
<div id="status">Downloading…</div>
|
||||||
|
<div class="track"><div id="bar"></div></div>
|
||||||
|
<button id="btn">Restart Now</button>
|
||||||
|
<script>
|
||||||
|
const {ipcRenderer}=require('electron');
|
||||||
|
ipcRenderer.on('upd-progress',(_,p)=>{
|
||||||
|
document.getElementById('bar').style.width=p+'%';
|
||||||
|
document.getElementById('status').textContent=p<100?'Downloading… '+p+'%':'Installing…';
|
||||||
|
});
|
||||||
|
ipcRenderer.on('upd-ready',(_,v)=>{
|
||||||
|
document.getElementById('title').textContent='Sanctum '+v+' installed';
|
||||||
|
document.getElementById('bar').style.width='100%';
|
||||||
|
var s=document.getElementById('status');
|
||||||
|
var n=2;
|
||||||
|
s.textContent='Restarting in '+n+'…';
|
||||||
|
var t=setInterval(function(){
|
||||||
|
n--;
|
||||||
|
if(n<=0){clearInterval(t);s.textContent='Restarting…';}
|
||||||
|
else s.textContent='Restarting in '+n+'…';
|
||||||
|
},1000);
|
||||||
|
});
|
||||||
|
ipcRenderer.on('upd-error',(_,msg)=>{
|
||||||
|
document.getElementById('title').textContent='Update Failed';
|
||||||
|
document.getElementById('status').textContent=msg;
|
||||||
|
document.getElementById('bar').style.background='#f38ba8';
|
||||||
|
});
|
||||||
|
</script></body></html>`;
|
||||||
|
|
||||||
|
export function showUpdateWindow() {
|
||||||
|
if (win) { win.focus(); return; }
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 400,
|
||||||
|
height: 180,
|
||||||
|
resizable: false,
|
||||||
|
minimizable: false,
|
||||||
|
maximizable: false,
|
||||||
|
fullscreenable: false,
|
||||||
|
title: "Sanctum Update",
|
||||||
|
frame: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(HTML));
|
||||||
|
win.on("closed", () => { win = null; });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUpdateProgress(percent: number) {
|
||||||
|
win?.webContents.send("upd-progress", Math.round(percent));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUpdateReady(version: string, relaunch: boolean) {
|
||||||
|
win?.webContents.send("upd-ready", version);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (relaunch) app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUpdateError(msg: string) {
|
||||||
|
win?.webContents.send("upd-error", msg);
|
||||||
|
}
|
||||||
154
src/native/updater.ts
Normal file
154
src/native/updater.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { Notification, app, ipcMain } from "electron";
|
||||||
|
import { exec, spawn } from "child_process";
|
||||||
|
import { createWriteStream, mkdirSync, writeFileSync } from "fs";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
|
||||||
|
import { showUpdateWindow, setUpdateProgress, setUpdateReady, setUpdateError } from "./update-window";
|
||||||
|
|
||||||
|
ipcMain.handle("checkForUpdates", () => checkForUpdates());
|
||||||
|
|
||||||
|
const RELEASES_URL =
|
||||||
|
"https://git.mithraic.cloud/api/v1/repos/ad3laid3/sanctum/releases/latest";
|
||||||
|
|
||||||
|
interface Asset {
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Release {
|
||||||
|
tag_name: string;
|
||||||
|
html_url: string;
|
||||||
|
assets: Asset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
console.log("[updater] checking:", RELEASES_URL);
|
||||||
|
|
||||||
|
const res = await fetch(RELEASES_URL);
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("[updater] releases API returned", res.status, res.statusText);
|
||||||
|
notify("Update Check Failed", `API returned ${res.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = (await res.json()) as Release;
|
||||||
|
const latest = release.tag_name.replace(/^v/, "");
|
||||||
|
const current = app.getVersion();
|
||||||
|
|
||||||
|
console.log(`[updater] current=${current} latest=${latest}`);
|
||||||
|
|
||||||
|
if (!isNewer(latest, current)) {
|
||||||
|
notify("Already Up to Date", `You're on Sanctum ${current}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = findAsset(release.assets);
|
||||||
|
if (!asset) {
|
||||||
|
const names = release.assets.map(a => a.name).join(", ");
|
||||||
|
console.error("[updater] no matching asset for platform:", process.platform, names);
|
||||||
|
notify("Update Failed", `No ${process.platform} asset found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[updater] update available: ${current} → ${latest}, downloading ${asset.name}`);
|
||||||
|
showUpdateWindow();
|
||||||
|
await downloadAndInstall(asset.browser_download_url, latest);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[updater] update check failed:", err);
|
||||||
|
setUpdateError(String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAsset(assets: Asset[]): Asset | undefined {
|
||||||
|
if (process.platform === "linux")
|
||||||
|
return assets.find((a) => a.name.includes("linux") && a.name.endsWith(".zip"));
|
||||||
|
if (process.platform === "win32")
|
||||||
|
return assets.find((a) => a.name.includes("win32") && a.name.endsWith(".zip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadAndInstall(url: string, version: string) {
|
||||||
|
const tmpDir = join(tmpdir(), `sanctum-update-${version}`);
|
||||||
|
mkdirSync(tmpDir, { recursive: true });
|
||||||
|
const zipPath = join(tmpDir, "update.zip");
|
||||||
|
const extractDir = join(tmpDir, "extracted");
|
||||||
|
const installDir = dirname(process.execPath);
|
||||||
|
|
||||||
|
console.log(`[updater] downloading from ${url}`);
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
||||||
|
|
||||||
|
const total = Number(res.headers.get("content-length")) || 0;
|
||||||
|
let downloaded = 0;
|
||||||
|
const writer = createWriteStream(zipPath);
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
writer.write(value);
|
||||||
|
downloaded += value.length;
|
||||||
|
if (total > 0) setUpdateProgress(Math.round((downloaded / total) * 90));
|
||||||
|
}
|
||||||
|
await new Promise<void>(resolve => writer.end(resolve));
|
||||||
|
|
||||||
|
setUpdateProgress(95);
|
||||||
|
console.log(`[updater] download complete, extracting to ${installDir}`);
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
// Extract zip while the app is still running (no locked files yet)
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const cmd = `powershell -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'"`;
|
||||||
|
exec(cmd, (err, _stdout, stderr) => {
|
||||||
|
if (err) { console.error("[updater] extract failed:", stderr); reject(err); }
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write a batch script that runs after we exit: waits, copies, relaunches
|
||||||
|
const batchPath = join(tmpDir, "apply-update.bat");
|
||||||
|
const bat = [
|
||||||
|
"@echo off",
|
||||||
|
"timeout /t 3 /nobreak >nul",
|
||||||
|
`for /d %%D in ("${extractDir}\\*") do set SUB=%%D`,
|
||||||
|
`xcopy /E /Y /I "%SUB%\\*" "${installDir}\\"`,
|
||||||
|
`start "" "${join(installDir, "sanctum.exe")}"`,
|
||||||
|
`del "%~f0"`,
|
||||||
|
].join("\r\n");
|
||||||
|
writeFileSync(batchPath, bat);
|
||||||
|
|
||||||
|
// Spawn batch truly detached so it outlives this process
|
||||||
|
const child = spawn("cmd.exe", ["/C", batchPath], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
windowsHide: false,
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
setUpdateReady(version, false); // batch script handles relaunch
|
||||||
|
} else {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const cmd = `unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && rm -f "${installDir}/sanctum" && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`;
|
||||||
|
exec(cmd, { shell: "/bin/bash" }, (err, _stdout, stderr) => {
|
||||||
|
if (err) { console.error("[updater] extract failed:", stderr); reject(err); }
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setUpdateReady(version, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(title: string, body: string) {
|
||||||
|
const n = new Notification({ title, body, silent: true });
|
||||||
|
n.show();
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNewer(latest: string, current: string): boolean {
|
||||||
|
const parse = (v: string) => v.split(".").map(p => Number(p) || 0);
|
||||||
|
const [lA, lB, lC] = parse(latest);
|
||||||
|
const [cA, cB, cC] = parse(current);
|
||||||
|
if (lA !== cA) return lA > cA;
|
||||||
|
if (lB !== cB) return lB > cB;
|
||||||
|
return lC > cC;
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,9 @@ import {
|
||||||
nativeImage,
|
nativeImage,
|
||||||
} from "electron";
|
} from "electron";
|
||||||
|
|
||||||
import windowIconAsset from "../../assets/desktop/icon.png?asset";
|
import windowIconAsset from "../../avia_assets/icon.png?asset";
|
||||||
|
|
||||||
|
import { aboutWindow, createAboutWindow } from "./about";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import { updateTrayMenu } from "./tray";
|
import { updateTrayMenu } from "./tray";
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ export let mainWindow: BrowserWindow;
|
||||||
export const BUILD_URL = new URL(
|
export const BUILD_URL = new URL(
|
||||||
app.commandLine.hasSwitch("force-server")
|
app.commandLine.hasSwitch("force-server")
|
||||||
? app.commandLine.getSwitchValue("force-server")
|
? app.commandLine.getSwitchValue("force-server")
|
||||||
: /*MAIN_WINDOW_VITE_DEV_SERVER_URL ??*/ "https://beta.revolt.chat",
|
: /*MAIN_WINDOW_VITE_DEV_SERVER_URL ??*/ "https://mithraic.space/app",
|
||||||
);
|
);
|
||||||
|
|
||||||
// internal window state
|
// internal window state
|
||||||
|
|
@ -48,6 +49,18 @@ export function createMainWindow() {
|
||||||
height: 720,
|
height: 720,
|
||||||
backgroundColor: "#191919",
|
backgroundColor: "#191919",
|
||||||
frame: !config.customFrame,
|
frame: !config.customFrame,
|
||||||
|
...(config.customFrame && config.customFrameNativeMenu
|
||||||
|
? {
|
||||||
|
// remove the default titlebar
|
||||||
|
titleBarStyle: "hidden",
|
||||||
|
// expose window controls in Windows/Linux
|
||||||
|
...(process.platform !== "darwin"
|
||||||
|
? {
|
||||||
|
titleBarOverlay: true,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
icon: windowIcon,
|
icon: windowIcon,
|
||||||
show: !startHidden,
|
show: !startHidden,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
|
@ -56,6 +69,7 @@ export function createMainWindow() {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
|
devTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -132,17 +146,60 @@ export function createMainWindow() {
|
||||||
// reset zoom to default.
|
// reset zoom to default.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.webContents.setZoomLevel(0);
|
mainWindow.webContents.setZoomLevel(0);
|
||||||
|
} else if (input.key === "F1") {
|
||||||
|
event.preventDefault();
|
||||||
|
createAboutWindow();
|
||||||
} else if (
|
} else if (
|
||||||
input.key === "F5" ||
|
input.key === "F5" ||
|
||||||
((input.control || input.meta) && input.key.toLowerCase() === "r")
|
((input.control || input.meta) && input.key.toLowerCase() === "r")
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.webContents.reload();
|
mainWindow.webContents.reload();
|
||||||
|
} else if (input.key === "F12") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (mainWindow.webContents.isDevToolsOpened()) {
|
||||||
|
mainWindow.webContents.closeDevTools();
|
||||||
|
} else {
|
||||||
|
mainWindow.webContents.openDevTools({ mode: "detach" });
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
input.meta &&
|
||||||
|
input.key === "," &&
|
||||||
|
process.platform === "darwin"
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.webContents.executeJavaScript(`(() => {
|
||||||
|
var escButton = document.querySelector("#floating .top_0 > button");
|
||||||
|
var settingsPanel = document.querySelector("#root div[aria-label='Settings'] > a");
|
||||||
|
|
||||||
|
if (escButton) escButton.click();
|
||||||
|
if (!escButton && settingsPanel) settingsPanel.click();
|
||||||
|
})();`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// send the config
|
const initialCustomFrame: boolean = config.customFrame;
|
||||||
mainWindow.webContents.on("did-finish-load", () => config.sync());
|
const initialCFNM: boolean = config.customFrameNativeMenu;
|
||||||
|
|
||||||
|
mainWindow.webContents.on("did-finish-load", () => {
|
||||||
|
// send the config
|
||||||
|
config.sync();
|
||||||
|
|
||||||
|
// on macOS add margin to the title, and hide custom controls
|
||||||
|
// We only use initial values other the menu can disappear
|
||||||
|
if (process.platform === "darwin" && initialCustomFrame && initialCFNM) {
|
||||||
|
mainWindow.webContents.insertCSS(`
|
||||||
|
#root > div[style="display: flex; flex-direction: column; height: 100%;"] > div > div.h_29px {
|
||||||
|
&> div.d_flex:first-child {
|
||||||
|
margin-left: 75px;
|
||||||
|
}
|
||||||
|
&> a.place-items_center {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// configure spellchecker context menu
|
// configure spellchecker context menu
|
||||||
mainWindow.webContents.on("context-menu", (_, params) => {
|
mainWindow.webContents.on("context-menu", (_, params) => {
|
||||||
|
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
(function () {
|
|
||||||
|
|
||||||
if (window.__AVIA_PLUGINS_LOADED__) return;
|
|
||||||
window.__AVIA_PLUGINS_LOADED__ = true;
|
|
||||||
|
|
||||||
const STORAGE_KEY = "avia_plugins";
|
|
||||||
|
|
||||||
const runningPlugins = {};
|
|
||||||
const pluginErrors = {};
|
|
||||||
const injectionQueue = [];
|
|
||||||
|
|
||||||
const getPlugins = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
|
||||||
const setPlugins = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
||||||
|
|
||||||
async function processQueue() {
|
|
||||||
if (processQueue.running) return;
|
|
||||||
processQueue.running = true;
|
|
||||||
while (injectionQueue.length) {
|
|
||||||
const { plugin, force } = injectionQueue.shift();
|
|
||||||
await loadPluginInternal(plugin, force);
|
|
||||||
}
|
|
||||||
processQueue.running = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function queuePlugin(plugin, force = false) {
|
|
||||||
injectionQueue.push({ plugin, force });
|
|
||||||
processQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPluginInternal(plugin, force = false) {
|
|
||||||
if (runningPlugins[plugin.url] && !force) return;
|
|
||||||
if (force) stopPlugin(plugin);
|
|
||||||
try {
|
|
||||||
const res = await fetch(plugin.url);
|
|
||||||
if (!res.ok) throw new Error("Fetch failed");
|
|
||||||
const code = await res.text();
|
|
||||||
delete pluginErrors[plugin.url];
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.textContent = code;
|
|
||||||
script.dataset.pluginUrl = plugin.url;
|
|
||||||
document.body.appendChild(script);
|
|
||||||
runningPlugins[plugin.url] = script;
|
|
||||||
} catch {
|
|
||||||
pluginErrors[plugin.url] = true;
|
|
||||||
}
|
|
||||||
renderPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopPlugin(plugin) {
|
|
||||||
const script = runningPlugins[plugin.url];
|
|
||||||
if (!script) return;
|
|
||||||
script.remove();
|
|
||||||
delete runningPlugins[plugin.url];
|
|
||||||
delete pluginErrors[plugin.url];
|
|
||||||
renderPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePluginsPanel() {
|
|
||||||
let panel = document.getElementById('avia-plugins-panel');
|
|
||||||
if (panel) {
|
|
||||||
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panel = document.createElement('div');
|
|
||||||
panel.id = 'avia-plugins-panel';
|
|
||||||
panel.style.position = 'fixed';
|
|
||||||
panel.style.bottom = '24px';
|
|
||||||
panel.style.right = '24px';
|
|
||||||
panel.style.width = '520px';
|
|
||||||
panel.style.height = '460px';
|
|
||||||
panel.style.background = 'var(--md-sys-color-surface, #1e1e1e)';
|
|
||||||
panel.style.color = 'var(--md-sys-color-on-surface, #fff)';
|
|
||||||
panel.style.borderRadius = '16px';
|
|
||||||
panel.style.boxShadow = '0 8px 28px rgba(0,0,0,0.35)';
|
|
||||||
panel.style.zIndex = '999999';
|
|
||||||
panel.style.display = 'flex';
|
|
||||||
panel.style.flexDirection = 'column';
|
|
||||||
panel.style.overflow = 'hidden';
|
|
||||||
panel.style.border = '1px solid rgba(255,255,255,0.08)';
|
|
||||||
panel.style.backdropFilter = 'blur(12px)';
|
|
||||||
const header = document.createElement('div');
|
|
||||||
header.textContent = 'Plugins';
|
|
||||||
header.style.padding = '14px 16px';
|
|
||||||
header.style.fontWeight = '600';
|
|
||||||
header.style.fontSize = '14px';
|
|
||||||
header.style.background = 'var(--md-sys-color-surface-container, rgba(255,255,255,0.04))';
|
|
||||||
header.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
|
|
||||||
header.style.cursor = 'move';
|
|
||||||
const closeBtn = document.createElement('div');
|
|
||||||
closeBtn.textContent = '✕';
|
|
||||||
closeBtn.style.position = 'absolute';
|
|
||||||
closeBtn.style.top = '12px';
|
|
||||||
closeBtn.style.right = '16px';
|
|
||||||
closeBtn.style.cursor = 'pointer';
|
|
||||||
closeBtn.style.opacity = '0.7';
|
|
||||||
closeBtn.onclick = () => panel.style.display = 'none';
|
|
||||||
const controlsBar = document.createElement('div');
|
|
||||||
controlsBar.style.padding = '12px 16px';
|
|
||||||
controlsBar.style.display = 'flex';
|
|
||||||
controlsBar.style.gap = '8px';
|
|
||||||
controlsBar.style.alignItems = 'center';
|
|
||||||
controlsBar.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
|
|
||||||
controlsBar.style.flex = '0 0 auto';
|
|
||||||
const content = document.createElement('div');
|
|
||||||
content.id = 'avia-plugins-content';
|
|
||||||
content.style.flex = '1';
|
|
||||||
content.style.overflow = 'auto';
|
|
||||||
content.style.padding = '16px';
|
|
||||||
const nameInput = document.createElement('input');
|
|
||||||
nameInput.placeholder = 'Name';
|
|
||||||
styleInput(nameInput);
|
|
||||||
nameInput.style.width = '110px';
|
|
||||||
const urlInput = document.createElement('input');
|
|
||||||
urlInput.placeholder = 'Plugin URL';
|
|
||||||
styleInput(urlInput);
|
|
||||||
urlInput.style.flex = '1';
|
|
||||||
const addBtn = document.createElement('button');
|
|
||||||
addBtn.textContent = 'Add';
|
|
||||||
addBtn.onclick = () => {
|
|
||||||
const name = nameInput.value.trim();
|
|
||||||
const url = urlInput.value.trim();
|
|
||||||
if (!name || !url) return;
|
|
||||||
const plugins = getPlugins();
|
|
||||||
plugins.push({ name, url, enabled: false });
|
|
||||||
setPlugins(plugins);
|
|
||||||
nameInput.value = '';
|
|
||||||
urlInput.value = '';
|
|
||||||
renderPanel();
|
|
||||||
};
|
|
||||||
const refreshAll = document.createElement('button');
|
|
||||||
refreshAll.textContent = 'Refresh';
|
|
||||||
refreshAll.onclick = () => {
|
|
||||||
const plugins = getPlugins();
|
|
||||||
plugins.forEach(p => {
|
|
||||||
if (p.enabled) queuePlugin(p, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
controlsBar.appendChild(nameInput);
|
|
||||||
controlsBar.appendChild(urlInput);
|
|
||||||
controlsBar.appendChild(addBtn);
|
|
||||||
controlsBar.appendChild(refreshAll);
|
|
||||||
panel.appendChild(header);
|
|
||||||
panel.appendChild(closeBtn);
|
|
||||||
panel.appendChild(controlsBar);
|
|
||||||
panel.appendChild(content);
|
|
||||||
document.body.appendChild(panel);
|
|
||||||
enableDrag(panel, header);
|
|
||||||
renderPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPanel() {
|
|
||||||
const content = document.getElementById('avia-plugins-content');
|
|
||||||
if (!content) return;
|
|
||||||
content.innerHTML = '';
|
|
||||||
const plugins = getPlugins();
|
|
||||||
const runningSnapshot = { ...runningPlugins };
|
|
||||||
const errorSnapshot = { ...pluginErrors };
|
|
||||||
plugins.forEach((plugin, index) => {
|
|
||||||
const isRunning = !!runningSnapshot[plugin.url];
|
|
||||||
const hasError = !!errorSnapshot[plugin.url];
|
|
||||||
const row = document.createElement('div');
|
|
||||||
row.style.display = 'flex';
|
|
||||||
row.style.justifyContent = 'space-between';
|
|
||||||
row.style.alignItems = 'center';
|
|
||||||
row.style.marginBottom = '12px';
|
|
||||||
const left = document.createElement('div');
|
|
||||||
left.style.display = 'flex';
|
|
||||||
left.style.alignItems = 'center';
|
|
||||||
left.style.gap = '10px';
|
|
||||||
const statusDot = document.createElement('div');
|
|
||||||
statusDot.style.width = '10px';
|
|
||||||
statusDot.style.height = '10px';
|
|
||||||
statusDot.style.borderRadius = '50%';
|
|
||||||
if (hasError) {
|
|
||||||
statusDot.style.background = '#ff4d4d';
|
|
||||||
statusDot.style.boxShadow = '0 0 6px #ff4d4d';
|
|
||||||
} else if (isRunning) {
|
|
||||||
statusDot.style.background = '#4dff88';
|
|
||||||
statusDot.style.boxShadow = '0 0 6px #4dff88';
|
|
||||||
} else {
|
|
||||||
statusDot.style.background = '#777';
|
|
||||||
}
|
|
||||||
const name = document.createElement('div');
|
|
||||||
name.textContent = plugin.name;
|
|
||||||
left.appendChild(statusDot);
|
|
||||||
left.appendChild(name);
|
|
||||||
const controls = document.createElement('div');
|
|
||||||
controls.style.display = 'flex';
|
|
||||||
controls.style.gap = '6px';
|
|
||||||
const toggle = document.createElement('button');
|
|
||||||
toggle.textContent = plugin.enabled ? 'Disable' : 'Enable';
|
|
||||||
toggle.onclick = () => {
|
|
||||||
plugin.enabled = !plugin.enabled;
|
|
||||||
setPlugins(plugins);
|
|
||||||
if (plugin.enabled) queuePlugin(plugin);
|
|
||||||
else stopPlugin(plugin);
|
|
||||||
renderPanel();
|
|
||||||
};
|
|
||||||
const remove = document.createElement('button');
|
|
||||||
remove.textContent = '✕';
|
|
||||||
remove.onclick = () => {
|
|
||||||
stopPlugin(plugin);
|
|
||||||
plugins.splice(index, 1);
|
|
||||||
setPlugins(plugins);
|
|
||||||
renderPanel();
|
|
||||||
};
|
|
||||||
controls.appendChild(toggle);
|
|
||||||
controls.appendChild(remove);
|
|
||||||
row.appendChild(left);
|
|
||||||
row.appendChild(controls);
|
|
||||||
content.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function styleInput(input) {
|
|
||||||
input.style.padding = '6px 8px';
|
|
||||||
input.style.borderRadius = '8px';
|
|
||||||
input.style.border = '1px solid rgba(255,255,255,0.1)';
|
|
||||||
input.style.background = 'rgba(255,255,255,0.05)';
|
|
||||||
input.style.color = '#fff';
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableDrag(panel, header) {
|
|
||||||
let isDragging = false, offsetX, offsetY;
|
|
||||||
header.addEventListener('mousedown', e => {
|
|
||||||
isDragging = true;
|
|
||||||
offsetX = e.clientX - panel.offsetLeft;
|
|
||||||
offsetY = e.clientY - panel.offsetTop;
|
|
||||||
});
|
|
||||||
document.addEventListener('mouseup', () => isDragging = false);
|
|
||||||
document.addEventListener('mousemove', e => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
panel.style.left = (e.clientX - offsetX) + 'px';
|
|
||||||
panel.style.top = (e.clientY - offsetY) + 'px';
|
|
||||||
panel.style.right = 'auto';
|
|
||||||
panel.style.bottom = 'auto';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectButtons() {
|
|
||||||
if (document.getElementById('stoat-fake-plugins')) return;
|
|
||||||
const appearanceBtn = [...document.querySelectorAll('a')]
|
|
||||||
.find(a => a.textContent.trim() === 'Appearance');
|
|
||||||
if (!appearanceBtn) return;
|
|
||||||
const referenceNode = document.getElementById('stoat-fake-quickcss');
|
|
||||||
if (!referenceNode) return;
|
|
||||||
const pluginsBtn = appearanceBtn.cloneNode(true);
|
|
||||||
pluginsBtn.id = 'stoat-fake-plugins';
|
|
||||||
const textNode = [...pluginsBtn.querySelectorAll('div')]
|
|
||||||
.find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
|
||||||
if (textNode) textNode.textContent = "(Avia) Plugins";
|
|
||||||
if (typeof setIcon === "function") setIcon(pluginsBtn, "extension");
|
|
||||||
pluginsBtn.addEventListener('click', togglePluginsPanel);
|
|
||||||
referenceNode.parentElement.insertBefore(pluginsBtn, referenceNode.nextSibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForBody(callback) {
|
|
||||||
if (document.body) callback();
|
|
||||||
else new MutationObserver((obs) => {
|
|
||||||
if (document.body) {
|
|
||||||
obs.disconnect();
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}).observe(document.documentElement, { childList: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForBody(() => {
|
|
||||||
const observer = new MutationObserver(() => injectButtons());
|
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
|
||||||
injectButtons();
|
|
||||||
});
|
|
||||||
|
|
||||||
getPlugins().forEach(plugin => {
|
|
||||||
if (plugin.enabled) queuePlugin(plugin);
|
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
|
||||||
266
src/themes.js
266
src/themes.js
|
|
@ -1,266 +0,0 @@
|
||||||
(function () {
|
|
||||||
|
|
||||||
if (window.__AVIA_THEMES_LOADED__) return;
|
|
||||||
window.__AVIA_THEMES_LOADED__ = true;
|
|
||||||
|
|
||||||
const STORAGE_KEY = "avia_themes";
|
|
||||||
let editingTheme = null;
|
|
||||||
|
|
||||||
const getThemes = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
|
||||||
const setThemes = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
||||||
|
|
||||||
function parseMeta(css){
|
|
||||||
const name = css.match(/@name\s+(.+)/)?.[1] || "Unknown Theme";
|
|
||||||
const author = css.match(/@author\s+(.+)/)?.[1] || "Unknown";
|
|
||||||
const version = css.match(/@version\s+(.+)/)?.[1] || "1.0";
|
|
||||||
const description = css.match(/@description\s+(.+)/)?.[1] || "No description";
|
|
||||||
return {name,author,version,description};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyThemes(){
|
|
||||||
document.querySelectorAll(".avia-theme-style").forEach(e=>e.remove());
|
|
||||||
const themes = getThemes();
|
|
||||||
themes.forEach(theme=>{
|
|
||||||
if(!theme.enabled) return;
|
|
||||||
const style=document.createElement("style");
|
|
||||||
style.className="avia-theme-style";
|
|
||||||
style.textContent=theme.css;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDraggable(panel, handle){
|
|
||||||
let dragging=false,offsetX,offsetY;
|
|
||||||
handle.addEventListener("mousedown",e=>{
|
|
||||||
dragging=true;
|
|
||||||
offsetX=e.clientX-panel.offsetLeft;
|
|
||||||
offsetY=e.clientY-panel.offsetTop;
|
|
||||||
document.body.style.userSelect="none";
|
|
||||||
});
|
|
||||||
document.addEventListener("mouseup",()=>{dragging=false;document.body.style.userSelect="";});
|
|
||||||
document.addEventListener("mousemove",e=>{
|
|
||||||
if(!dragging) return;
|
|
||||||
panel.style.left=(e.clientX-offsetX)+"px";
|
|
||||||
panel.style.top=(e.clientY-offsetY)+"px";
|
|
||||||
panel.style.right="auto";
|
|
||||||
panel.style.bottom="auto";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openThemeEditor(theme){
|
|
||||||
editingTheme = theme;
|
|
||||||
let panel = document.getElementById('avia-theme-editor');
|
|
||||||
if(panel){
|
|
||||||
panel.style.display="flex";
|
|
||||||
panel.querySelector("textarea").value = theme.css;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panel=document.createElement("div");
|
|
||||||
panel.id="avia-theme-editor";
|
|
||||||
Object.assign(panel.style,{
|
|
||||||
position:"fixed",
|
|
||||||
bottom:"24px",
|
|
||||||
right:"24px",
|
|
||||||
width:"420px",
|
|
||||||
height:"340px",
|
|
||||||
background:"var(--md-sys-color-surface,#1e1e1e)",
|
|
||||||
color:"var(--md-sys-color-on-surface,#fff)",
|
|
||||||
borderRadius:"16px",
|
|
||||||
boxShadow:"0 8px 28px rgba(0,0,0,0.35)",
|
|
||||||
zIndex:999999,
|
|
||||||
display:"flex",
|
|
||||||
flexDirection:"column",
|
|
||||||
overflow:"hidden",
|
|
||||||
border:"1px solid rgba(255,255,255,0.08)"
|
|
||||||
});
|
|
||||||
const header=document.createElement("div");
|
|
||||||
header.textContent="Theme Editor";
|
|
||||||
Object.assign(header.style,{
|
|
||||||
padding:"14px 16px",
|
|
||||||
fontWeight:"600",
|
|
||||||
fontSize:"14px",
|
|
||||||
background:"rgba(255,255,255,0.04)",
|
|
||||||
borderBottom:"1px solid rgba(255,255,255,0.08)",
|
|
||||||
cursor:"move"
|
|
||||||
});
|
|
||||||
makeDraggable(panel,header);
|
|
||||||
const close=document.createElement("div");
|
|
||||||
close.textContent="✕";
|
|
||||||
Object.assign(close.style,{position:"absolute",right:"16px",top:"12px",cursor:"pointer"});
|
|
||||||
close.onclick=()=>panel.style.display="none";
|
|
||||||
const textarea=document.createElement("textarea");
|
|
||||||
Object.assign(textarea.style,{
|
|
||||||
flex:"1",
|
|
||||||
border:"none",
|
|
||||||
outline:"none",
|
|
||||||
resize:"none",
|
|
||||||
padding:"16px",
|
|
||||||
background:"transparent",
|
|
||||||
color:"inherit",
|
|
||||||
fontFamily:"monospace",
|
|
||||||
fontSize:"13px"
|
|
||||||
});
|
|
||||||
textarea.value=theme.css;
|
|
||||||
textarea.addEventListener("input",()=>{
|
|
||||||
const themes=getThemes();
|
|
||||||
const t=themes.find(x=>x.id===editingTheme.id);
|
|
||||||
if(!t) return;
|
|
||||||
t.css=textarea.value;
|
|
||||||
setThemes(themes);
|
|
||||||
applyThemes();
|
|
||||||
if(window.__avia_refresh_themes_panel){window.__avia_refresh_themes_panel();}
|
|
||||||
});
|
|
||||||
panel.appendChild(header);
|
|
||||||
panel.appendChild(close);
|
|
||||||
panel.appendChild(textarea);
|
|
||||||
document.body.appendChild(panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleThemesPanel(){
|
|
||||||
let panel=document.getElementById("avia-themes-panel");
|
|
||||||
if(panel){
|
|
||||||
panel.style.display = panel.style.display==="none"?"flex":"none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panel=document.createElement("div");
|
|
||||||
panel.id="avia-themes-panel";
|
|
||||||
Object.assign(panel.style,{
|
|
||||||
position:"fixed",
|
|
||||||
bottom:"40px",
|
|
||||||
right:"40px",
|
|
||||||
width:"500px",
|
|
||||||
height:"380px",
|
|
||||||
background:"#1e1e1e",
|
|
||||||
color:"#fff",
|
|
||||||
borderRadius:"16px",
|
|
||||||
boxShadow:"0 12px 35px rgba(0,0,0,0.45)",
|
|
||||||
zIndex:999999,
|
|
||||||
display:"flex",
|
|
||||||
flexDirection:"column",
|
|
||||||
overflow:"hidden",
|
|
||||||
border:"1px solid rgba(255,255,255,0.08)"
|
|
||||||
});
|
|
||||||
const header=document.createElement("div");
|
|
||||||
header.textContent="Themes";
|
|
||||||
Object.assign(header.style,{
|
|
||||||
padding:"14px 16px",
|
|
||||||
fontWeight:"600",
|
|
||||||
fontSize:"14px",
|
|
||||||
background:"rgba(255,255,255,0.04)",
|
|
||||||
borderBottom:"1px solid rgba(255,255,255,0.08)",
|
|
||||||
cursor:"move"
|
|
||||||
});
|
|
||||||
makeDraggable(panel,header);
|
|
||||||
const close=document.createElement("div");
|
|
||||||
close.textContent="✕";
|
|
||||||
Object.assign(close.style,{position:"absolute",right:"16px",top:"12px",cursor:"pointer"});
|
|
||||||
close.onclick=()=>panel.style.display="none";
|
|
||||||
const importBtn=document.createElement("button");
|
|
||||||
importBtn.textContent="Import Theme";
|
|
||||||
Object.assign(importBtn.style,{
|
|
||||||
margin:"10px",
|
|
||||||
padding:"10px",
|
|
||||||
borderRadius:"8px",
|
|
||||||
border:"1px solid rgba(255,255,255,0.1)",
|
|
||||||
background:"rgba(255,255,255,0.06)",
|
|
||||||
color:"#fff",
|
|
||||||
fontWeight:"500",
|
|
||||||
cursor:"pointer",
|
|
||||||
transition:"all .15s ease"
|
|
||||||
});
|
|
||||||
importBtn.onmouseenter=()=>{importBtn.style.background="rgba(255,255,255,0.12)";};
|
|
||||||
importBtn.onmouseleave=()=>{importBtn.style.background="rgba(255,255,255,0.06)";};
|
|
||||||
const list=document.createElement("div");
|
|
||||||
Object.assign(list.style,{
|
|
||||||
flex:"1",
|
|
||||||
overflowY:"auto",
|
|
||||||
padding:"12px",
|
|
||||||
display:"flex",
|
|
||||||
flexDirection:"column",
|
|
||||||
gap:"8px"
|
|
||||||
});
|
|
||||||
panel.appendChild(header);
|
|
||||||
panel.appendChild(close);
|
|
||||||
panel.appendChild(importBtn);
|
|
||||||
panel.appendChild(list);
|
|
||||||
document.body.appendChild(panel);
|
|
||||||
function render(){
|
|
||||||
list.innerHTML="";
|
|
||||||
const themes=getThemes();
|
|
||||||
themes.forEach(theme=>{
|
|
||||||
const meta=parseMeta(theme.css);
|
|
||||||
const card=document.createElement("div");
|
|
||||||
Object.assign(card.style,{
|
|
||||||
padding:"10px",
|
|
||||||
borderRadius:"10px",
|
|
||||||
background:"rgba(255,255,255,0.05)",
|
|
||||||
display:"flex",
|
|
||||||
justifyContent:"space-between",
|
|
||||||
alignItems:"center"
|
|
||||||
});
|
|
||||||
const info=document.createElement("div");
|
|
||||||
info.innerHTML=`<div style="font-weight:600">${meta.name}</div><div style="font-size:11px;opacity:.7">${meta.author} • v${meta.version}</div><div style="font-size:11px;opacity:.6">${meta.description}</div>`;
|
|
||||||
const controls=document.createElement("div");
|
|
||||||
const toggle=document.createElement("button");
|
|
||||||
toggle.textContent=theme.enabled?"Disable":"Enable";
|
|
||||||
toggle.onclick=()=>{
|
|
||||||
theme.enabled=!theme.enabled;
|
|
||||||
setThemes(themes);
|
|
||||||
applyThemes();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
const edit=document.createElement("button");
|
|
||||||
edit.textContent="Edit";
|
|
||||||
edit.onclick=()=>openThemeEditor(theme);
|
|
||||||
const del=document.createElement("button");
|
|
||||||
del.textContent="Delete";
|
|
||||||
del.onclick=()=>{
|
|
||||||
const updated=themes.filter(t=>t.id!==theme.id);
|
|
||||||
setThemes(updated);
|
|
||||||
applyThemes();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
[toggle,edit,del].forEach(b=>{Object.assign(b.style,{marginLeft:"6px",padding:"4px 8px",borderRadius:"6px",border:"none",cursor:"pointer"});controls.appendChild(b);});
|
|
||||||
card.appendChild(info);
|
|
||||||
card.appendChild(controls);
|
|
||||||
list.appendChild(card);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
window.__avia_refresh_themes_panel = render;
|
|
||||||
importBtn.onclick=()=>{
|
|
||||||
const input=document.createElement("input");
|
|
||||||
input.type="file";
|
|
||||||
input.accept=".css,.txt";
|
|
||||||
input.onchange=async()=>{
|
|
||||||
const file=input.files[0];
|
|
||||||
if(!file) return;
|
|
||||||
const css=await file.text();
|
|
||||||
const themes=getThemes();
|
|
||||||
themes.push({id:crypto.randomUUID(),css,enabled:true});
|
|
||||||
setThemes(themes);
|
|
||||||
applyThemes();
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
};
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectButton(){
|
|
||||||
if(document.getElementById("avia-themes-btn")) return;
|
|
||||||
const appearanceBtn=[...document.querySelectorAll("a")].find(a=>a.textContent.trim()==="Appearance");
|
|
||||||
const quickCSS=document.getElementById("stoat-fake-quickcss");
|
|
||||||
if(!appearanceBtn || !quickCSS) return;
|
|
||||||
const clone=appearanceBtn.cloneNode(true);
|
|
||||||
clone.id="avia-themes-btn";
|
|
||||||
const text=[...clone.querySelectorAll("div")].find(d=>d.children.length===0);
|
|
||||||
if(text) text.textContent="(Avia) Themes";
|
|
||||||
clone.onclick=toggleThemesPanel;
|
|
||||||
quickCSS.parentElement.insertBefore(clone, quickCSS.nextSibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
new MutationObserver(injectButton).observe(document.body,{childList:true,subtree:true});
|
|
||||||
injectButton();
|
|
||||||
applyThemes();
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
import { version } from "../../package.json";
|
import { aviaVersion, version } from "../../package.json";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("native", {
|
contextBridge.exposeInMainWorld("native", {
|
||||||
versions: {
|
versions: {
|
||||||
|
|
@ -8,6 +8,23 @@ contextBridge.exposeInMainWorld("native", {
|
||||||
chrome: () => process.versions.chrome,
|
chrome: () => process.versions.chrome,
|
||||||
electron: () => process.versions.electron,
|
electron: () => process.versions.electron,
|
||||||
desktop: () => version,
|
desktop: () => version,
|
||||||
|
aviaClient: () => aviaVersion,
|
||||||
|
},
|
||||||
|
|
||||||
|
overlay: {
|
||||||
|
setVoiceState: (state: VoiceOverlayState | null) =>
|
||||||
|
ipcRenderer.send("overlay:set-voice-state", state),
|
||||||
|
},
|
||||||
|
|
||||||
|
activity: {
|
||||||
|
getState: () => ipcRenderer.invoke("sanctum-activity:get-state"),
|
||||||
|
onUpdate: (callback: (state: SanctumActivityState) => void) => {
|
||||||
|
const listener = (_event: unknown, state: SanctumActivityState) => callback(state);
|
||||||
|
ipcRenderer.on("sanctum-activity:update", listener);
|
||||||
|
return () => ipcRenderer.removeListener("sanctum-activity:update", listener);
|
||||||
|
},
|
||||||
|
debugSetState: (state: SanctumActivityState) =>
|
||||||
|
ipcRenderer.invoke("sanctum-activity:debug-set-state", state) as Promise<SanctumActivityState>,
|
||||||
},
|
},
|
||||||
|
|
||||||
minimise: () => ipcRenderer.send("minimise"),
|
minimise: () => ipcRenderer.send("minimise"),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue