Compare commits
29 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c4ffbc00 | ||
|
|
fa250dda5f | ||
|
|
23823a9b59 | ||
|
|
64dbb318b0 | ||
|
|
9385499c4b | ||
|
|
7e90058e7a | ||
|
|
ec61bbe5d5 | ||
|
|
d1bba69bcf | ||
|
|
5e94d75a3e | ||
|
|
6ada11778a | ||
|
|
c828a6af47 | ||
|
|
581a853b6f | ||
|
|
f9b7a9739f | ||
|
|
7662f00723 | ||
|
|
4fb5461321 | ||
|
|
04cae9d50b | ||
|
|
fb9d583c71 | ||
|
|
25543cb7ba | ||
|
|
e259d9b63c | ||
|
|
17da0c0234 | ||
|
|
2379562aec | ||
|
|
376dc56e52 | ||
|
|
28eed3e03d | ||
|
|
7d1446302d | ||
|
|
7f54531db5 | ||
|
|
c6ed6b5074 | ||
|
|
85e5b16938 | ||
|
|
45f45dc4b9 | ||
|
|
d1a46defd5 |
63
.gitea/workflows/build.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
name: Build & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set version from tag
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json')); p.version='$VERSION'; fs.writeFileSync('package.json', JSON.stringify(p, null, 2)+'\n')"
|
||||
|
||||
- name: Install Linux build deps
|
||||
run: apt-get update && apt-get install -y --fix-missing python3 make g++ libgtk-3-dev libnss3-dev libxss1 libasound2-dev libgbm-dev dpkg fakeroot zip
|
||||
|
||||
- name: Build Linux (deb + zip)
|
||||
run: pnpm make
|
||||
env:
|
||||
PLATFORM: linux
|
||||
|
||||
- name: Build Windows (zip)
|
||||
run: pnpm make --platform win32 --arch x64
|
||||
env:
|
||||
PLATFORM: win32
|
||||
|
||||
- name: Create release and upload artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||
|
||||
RELEASE_ID=$(curl -sf -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API/releases" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"Sanctum $TAG\"}" \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
|
||||
find out/make -name "*.deb" -o -name "*.zip" | while read file; do
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
"$API/releases/$RELEASE_ID/assets?name=$(basename "$file")" \
|
||||
--data-binary @"$file"
|
||||
done
|
||||
88
.github/workflows/build.yml
vendored
|
|
@ -1,20 +1,13 @@
|
|||
name: Build and Release Sanctum
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build App
|
||||
runs-on: docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -23,80 +16,13 @@ jobs:
|
|||
- name: Checkout assets
|
||||
run: git -c submodule."assets".update=checkout submodule update --init assets
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Linux & Windows
|
||||
# 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
|
||||
- name: Build
|
||||
run: pnpm run package
|
||||
|
|
|
|||
88
.github/workflows/release-please.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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 }}
|
||||
2
.github/workflows/release-webhook.yml
vendored
|
|
@ -11,7 +11,7 @@ on:
|
|||
jobs:
|
||||
release-webhook:
|
||||
name: Send Release Webhook
|
||||
runs-on: docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Send release notification webhook
|
||||
|
|
|
|||
2
.github/workflows/validate-pr-title.yml
vendored
|
|
@ -14,7 +14,7 @@ on:
|
|||
jobs:
|
||||
main:
|
||||
name: Validate PR title
|
||||
runs-on: docker
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
|
|
|
|||
12
GEMINI.md
|
|
@ -1,12 +0,0 @@
|
|||
# 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.
|
||||
94
README.md
|
|
@ -1,75 +1,59 @@
|
|||
<div align="center">
|
||||
<h1>
|
||||
Avia Client for Desktop
|
||||
"stoat desktop"
|
||||
</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
|
||||
|
||||
<img src="assets/desktop/icon.png" width="120" alt="Sanctum" />
|
||||
|
||||
# Sanctum
|
||||
|
||||
**The desktop client for [mithraic.space](https://mithraic.space)**
|
||||
Private. Self-hosted. No tracking, no telemetry, no nonsense.
|
||||
Windows & Linux.
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
## Installation
|
||||
---
|
||||
|
||||
<a href="https://repology.org/project/stoat-desktop/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/stoat-desktop.svg" alt="Packaging status" align="right">
|
||||
</a>
|
||||
## What is Sanctum?
|
||||
|
||||
- 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
|
||||
Sanctum is the official desktop client for **mithraic.space** — a private, self-hosted chat community. It's hardwired to connect exclusively to mithraic.space and ships with custom branding and a fully self-contained auto-update pipeline hosted on a private Forgejo instance.
|
||||
|
||||
## Development Guide
|
||||
Built on top of [Stoat for Desktop](https://github.com/stoatchat/for-desktop), which is itself built on the open-source [Revolt](https://revolt.chat) platform.
|
||||
|
||||
_Contribution guidelines for Desktop app TBA!_
|
||||
No reliance on external services. Everything stays in-house.
|
||||
|
||||
<!-- Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html), the [code style guidelines](./GUIDELINES.md), and the [technical documentation for this project](https://revoltchat.github.io/frontend/). -->
|
||||
---
|
||||
|
||||
Before getting started, you'll want to install:
|
||||
## Installing
|
||||
|
||||
- Git
|
||||
- Node.js
|
||||
- pnpm (run `corepack enable`)
|
||||
Grab the latest build from the [Releases page](https://git.mithraic.cloud/ad3laid3/sanctum/releases).
|
||||
|
||||
Then proceed to setup:
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
# clone the repository
|
||||
git clone --recursive https://github.com/AvaLilac/for-desktop aviaclient-for-desktop
|
||||
mkdir -p ~/.local/share/sanctum
|
||||
unzip Sanctum-linux-x64-*.zip -d /tmp/sanctum-extract
|
||||
cp -rT /tmp/sanctum-extract/Sanctum-linux-x64 ~/.local/share/sanctum/
|
||||
|
||||
# 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
|
||||
|
||||
# install all packages
|
||||
pnpm i --frozen-lockfile
|
||||
|
||||
# update the assets. if you are using stoat's
|
||||
git -c submodule."assets".update=checkout submodule update --init assets
|
||||
|
||||
# build the bundle
|
||||
pnpm package
|
||||
cat > ~/.local/share/applications/sanctum.desktop << EOF
|
||||
[Desktop Entry]
|
||||
Name=Sanctum
|
||||
Exec=$HOME/.local/share/sanctum/sanctum
|
||||
Icon=$HOME/.local/share/sanctum/resources/assets/desktop/icon.png
|
||||
Type=Application
|
||||
Categories=Network;InstantMessaging;
|
||||
StartupWMClass=sanctum
|
||||
EOF
|
||||
```
|
||||
|
||||
Various useful commands for development testing:
|
||||
Search for **Sanctum** in your app launcher and you're in.
|
||||
|
||||
```bash
|
||||
# connect to the development server
|
||||
pnpm start -- --force-server http://localhost:5173
|
||||
### Windows
|
||||
|
||||
# test the flatpak (after `make`)
|
||||
pnpm install:flatpak
|
||||
pnpm run:flatpak
|
||||
# ... also connect to dev server like so:
|
||||
pnpm run:flatpak --force-server http://localhost:5173
|
||||
Extract the zip, run `sanctum.exe`.
|
||||
|
||||
# Nix-specific instructions for testing
|
||||
pnpm package
|
||||
pnpm run:nix
|
||||
# ... as before:
|
||||
pnpm run:nix --force-server=http://localhost:5173
|
||||
# a better solution would be telling
|
||||
# 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.
|
||||
## Auto-updates
|
||||
|
||||
Sanctum checks for updates on every launch. When a new version is available, you'll get a desktop notification — it downloads and installs in the background. Click the notification to restart into the new version.
|
||||
|
||||
You can also trigger a manual check anytime from the tray icon → **Check for Updates**.
|
||||
|
|
|
|||
611
about.html
|
|
@ -1,611 +0,0 @@
|
|||
<!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>
|
||||
1
assets
|
|
@ -1 +0,0 @@
|
|||
Subproject commit bd432f2298901a8566a092636eef0c35a3a80fbc
|
||||
1
assets/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Assets intended for direct use in applications.
|
||||
7
assets/badges/amog.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="292" height="412" viewBox="0 0 292 412" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M123.504 267.035L128.535 258.859L138.241 252.156L154.5 249.777L183.052 252.156L197.725 256.518L203.277 269.604V287.788L197.725 312.316L188.604 316.718L165.604 319.571L154.5 333.698V350.899L169.966 356.451L197.725 361.606L217.156 364.382L243.726 356.451L269.409 342.968L273.812 319.571L276.243 287.788V249.777L273.812 197.227L269.409 151.317L264.378 118.614L258.718 97.2312L246.769 76.4775L234.191 56.9815L212.179 47.548L185.765 43.7746H161.238L137.969 47.548L123.504 56.9815L111.555 84.0243L97.7191 126.79L86.3988 180.246L71.9341 262.003L63.1295 290.304H57.4694L38.6023 287.788H26.0243L15.9619 295.335L10.3017 307.284L15.9619 316.718L31.0555 328.667L57.4694 333.698L105.895 328.667L128.535 312.316V299.109L123.504 280.242V267.035Z" fill="#BD0F11" stroke="black" stroke-width="10" stroke-linejoin="round"/>
|
||||
<rect x="143.273" y="91.2935" width="76.4135" height="25.6717" fill="#9DC7D7"/>
|
||||
<path d="M138.788 96.1069L145.638 109.56L154.76 115.762L166.846 118.169L180.083 120.174H191.335C194.544 119.372 199.78 119.171 202.767 118.169C205.754 117.166 208.048 116.43 209.987 115.762H214.6L218.01 111.951L222.622 104.53" stroke="#4E626B" stroke-width="10" stroke-linejoin="round"/>
|
||||
<path d="M148.155 123.275L135.465 111.775V99.4813L140.224 91.1536L148.155 86.7914H160.845H175.518H189.794H202.087L212.398 91.1536L221.518 99.4813L223.898 108.999L217.949 118.119L208.035 123.275L186.225 127.24H165.604L148.155 123.275Z" stroke="black" stroke-width="10" stroke-linejoin="round"/>
|
||||
<path d="M176.895 99.9176L174.287 98.1125V96.9092L176.895 95.7058H181.708H186.923H191.536L196.75 96.9092L200.36 98.1125L204.572 99.9176L202.767 101.121H196.75H191.536H186.12L181.708 99.9176H176.895Z" fill="white" stroke="white" stroke-width="4" stroke-linejoin="bevel"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
3018
assets/badges/amorbus.svg
Normal file
|
After Width: | Height: | Size: 258 KiB |
245
assets/badges/battlepass.svg
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#FFEB84;}
|
||||
.st2{fill:#FFFD80;}
|
||||
.st3{fill:#FFFFC0;}
|
||||
.st4{fill:#FFFFE8;}
|
||||
.st5{fill:#E1AF21;}
|
||||
.st6{fill:#FFDA51;}
|
||||
.st7{fill:#FFFBC0;}
|
||||
.st8{fill:#FFE27B;}
|
||||
.st9{fill:#DEB142;}
|
||||
.st10{fill:url(#SVGID_1_);}
|
||||
.st11{fill:#FDDB4A;}
|
||||
.st12{fill:#FFED89;}
|
||||
.st13{fill:#AB6D10;}
|
||||
.st14{fill:#ECBF39;}
|
||||
.st15{fill:#FFDA63;}
|
||||
.st16{fill:#D6A442;}
|
||||
.st17{fill:#C28631;}
|
||||
.st18{fill:#652C07;}
|
||||
.st19{fill:#C67726;}
|
||||
.st20{fill:#875117;}
|
||||
.st21{fill:#E0A053;}
|
||||
.st22{fill:#D68931;}
|
||||
.st23{fill:#945108;}
|
||||
.st24{fill:#C1823C;}
|
||||
.st25{fill:#FFC571;}
|
||||
.st26{fill:#DA9D52;}
|
||||
.st27{fill:#FCB54F;}
|
||||
.st28{fill:#BA6D1C;}
|
||||
.st29{fill:#ECCE82;}
|
||||
.st30{fill:#E9AF5C;}
|
||||
.st31{fill:#FFE173;}
|
||||
.st32{fill:#FFFFC8;}
|
||||
.st33{fill:#CE8229;}
|
||||
.st34{fill:url(#SVGID_00000170236236534105620470000017283433196162258594_);}
|
||||
.st35{fill:url(#SVGID_00000154411126143589039630000017497239855776706224_);}
|
||||
.st36{fill:#532308;}
|
||||
.st37{fill:#683218;}
|
||||
.st38{fill:#844929;}
|
||||
.st39{fill:#582309;}
|
||||
.st40{fill:#7A4021;}
|
||||
.st41{fill:#783F21;}
|
||||
.st42{fill:#5A250A;}
|
||||
.st43{fill:#3C0F07;}
|
||||
.st44{fill:#4F1E06;}
|
||||
.st45{fill:#210308;}
|
||||
.st46{fill:url(#SVGID_00000146478660205493002300000009688568192017483139_);}
|
||||
.st47{fill:url(#SVGID_00000010288141256007762670000007620850369090276751_);}
|
||||
.st48{fill:#944918;}
|
||||
.st49{fill:#773810;}
|
||||
.st50{fill:#AD6121;}
|
||||
.st51{fill:url(#SVGID_00000034800115922175574390000013293002930374637188_);}
|
||||
.st52{fill:#FFFFD9;}
|
||||
.st53{fill:#8C4918;}
|
||||
.st54{fill:#5C2D0E;}
|
||||
.st55{fill:#EFDC84;}
|
||||
.st56{fill:#FFF7DB;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
<g>
|
||||
<polygon class="st0" points="500,74 324,250 135,193 270,670 340,705 500,746 "/>
|
||||
<path class="st1" d="M324,250l-82.5,97.5c22.9-3.8,110.1-21,174.5-100.5c48.6-60,58.5-123.7,61.1-150.1C426,148,375,199,324,250z"
|
||||
/>
|
||||
<polygon class="st2" points="283.7,237.9 241.5,347.5 324,250 "/>
|
||||
<polygon class="st3" points="185.3,208.2 241.5,347.5 283.7,237.9 "/>
|
||||
<polygon class="st4" points="135,193 241.5,347.5 185.3,208.2 "/>
|
||||
<path class="st5" d="M244.2,579c4.5,4.7,28,32,28.8,33c7.9,10.9,33.7,40.9,74.5,68.5C406,720,458.6,734.7,456,734
|
||||
c-38.6-9.9-77.4-19.1-116-29l-70-35L244.2,579z"/>
|
||||
<polygon class="st6" points="241.5,347.5 283.7,626.3 310.5,588.5 "/>
|
||||
<polygon class="st7" points="500,705.1 283.7,626.3 310.5,588.5 500,646.8 "/>
|
||||
<path class="st8" d="M500,746c-31-6.5-75-19.3-122.5-46.5c-42.3-24.2-73.1-51.7-93.8-73.2c72.1,26.3,144.2,52.6,216.3,78.8V746z"
|
||||
/>
|
||||
<polygon class="st9" points="135,193 241.5,347.5 283.7,626.3 244.2,579 "/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="393.5" y1="180.5" x2="806.5" y2="593.5">
|
||||
<stop offset="0" style="stop-color:#FDDB4A"/>
|
||||
<stop offset="1" style="stop-color:#C28631"/>
|
||||
</linearGradient>
|
||||
<polygon class="st10" points="500,74 676,250 865,193 730,670 660,705 500,746 "/>
|
||||
<polygon class="st11" points="716.3,237.9 758.5,347.5 676,250 "/>
|
||||
<polygon class="st12" points="814.7,208.2 758.5,347.5 716.3,237.9 "/>
|
||||
<polygon class="st4" points="865,193 758.5,347.5 814.7,208.2 "/>
|
||||
<path class="st13" d="M755.8,579c-4.5,4.7-28,32-28.8,33c-7.9,10.9-33.7,40.9-74.5,68.5C594,720,541.4,734.7,544,734
|
||||
c38.6-9.9,77.4-19.1,116-29l70-35L755.8,579z"/>
|
||||
<polygon class="st14" points="758.5,347.5 716.3,626.3 689.5,588.5 "/>
|
||||
<polygon class="st15" points="500,705.1 716.3,626.3 689.5,588.5 500,646.8 "/>
|
||||
<path class="st16" d="M500,746c31-6.5,75-19.3,122.5-46.5c42.3-24.2,73.1-51.7,93.8-73.2c-72.1,26.3-144.2,52.6-216.3,78.8V746z"/>
|
||||
<polygon class="st17" points="865,193 758.5,347.5 716.3,626.3 755.8,579 "/>
|
||||
<g>
|
||||
<path class="st18" d="M372,741l24.9,61.1l30.4,10.9l38.2-37.6c-18-5.9-36.5-12.3-55.5-19.5C396.9,751.1,384.3,746.1,372,741z"/>
|
||||
<polygon class="st19" points="408.5,831.5 396.9,802.1 427.3,813.1 "/>
|
||||
<path class="st20" d="M305.9,753.3L372,741l24.9,61.1c-14.1-5.3-30-12.1-46.9-21.1C332.9,771.8,318.2,762.3,305.9,753.3z"/>
|
||||
<path class="st21" d="M256.7,753.4l49.1-0.2c12.9,9.1,28,18.6,45.1,27.7c16.3,8.7,31.8,15.6,45.9,21.1c3.9,9.8,7.7,19.6,11.6,29.4
|
||||
C387,825.8,361,817,333,803C300.9,787,275.5,769,256.7,753.4z"/>
|
||||
<path class="st22" d="M227.6,727.1l19.8-22.9L277,731c-8,0-16.9-0.2-26-1C242.7,729.3,234.9,728.3,227.6,727.1z"/>
|
||||
<path class="st23" d="M341.5,724.2c-11.1,1.9-22.9,3.5-35.5,4.8c-10.1,1-19.8,1.6-29,2c-9.9-8.9-19.8-17.8-29.6-26.7l28.6-33
|
||||
L341.5,724.2z"/>
|
||||
<polygon class="st24" points="209,658 247.4,704.3 276,671.2 "/>
|
||||
<path class="st25" d="M162.8,643.1c7.5,2.7,15.2,5.3,23.2,7.9c7.8,2.5,15.5,4.8,23,7c12.8,15.4,25.6,30.8,38.4,46.3l-19.8,22.9
|
||||
C215.8,715.9,202.8,702,190,685C178.8,670.2,169.9,655.9,162.8,643.1z"/>
|
||||
<path class="st26" d="M162.1,546.4c1.8,7.3,3.7,14.8,5.9,22.6c2.2,7.7,4.5,15.1,6.8,22.2c10.9-6.5,22.2-12.6,33.2-19.2
|
||||
L162.1,546.4z"/>
|
||||
<path class="st27" d="M145.3,609.6l29.5-18.5l17.6,39c-7.6-2.1-16.2-5-25.4-9.1C158.7,617.3,151.5,613.4,145.3,609.6z"/>
|
||||
<path class="st28" d="M174.8,591.2L208,572c5.1,10.8,10.7,21.9,18,34c8.5,14.1,17.2,26.5,25.5,37.1c-19.7-4.3-39.4-8.7-59.1-13
|
||||
L174.8,591.2z"/>
|
||||
<path class="st28" d="M223,608"/>
|
||||
<path class="st29" d="M111.9,508c6.2,5.6,12.9,11.3,20.1,17c10.4,8.2,20.5,15.3,30.1,21.4c4.3,14.9,8.5,29.8,12.8,44.7l-29.5,18.5
|
||||
c-6.9-13.2-14.1-29.1-20.3-47.6C118.3,542,114.3,523.6,111.9,508z"/>
|
||||
<polygon class="st30" points="159,430 178.5,452.5 156.5,456.2 "/>
|
||||
<path class="st31" d="M106.3,469l50.2-12.8l0.6,57.1c-7.9-5.4-16.3-11.8-25-19.3C122,485.4,113.5,476.9,106.3,469z"/>
|
||||
<path class="st32" d="M108.5,357.5L159,430l-2.5,26.2L106.3,469c-1.7-16.9-2.7-36-2.3-57C104.5,391.9,106.2,373.7,108.5,357.5z"/>
|
||||
<path class="st33" d="M156.5,456.2l22-3.7c0.5,13,1.9,28.4,5.5,45.5c3.3,15.6,7.5,29.2,11.9,40.6c-12.9-8.4-25.9-16.9-38.8-25.3
|
||||
c-0.6-9.6-1-19.7-1-30.3C155.9,473.7,156.1,464.8,156.5,456.2z"/>
|
||||
<path class="st25" d="M184,647"/>
|
||||
|
||||
<linearGradient id="SVGID_00000181052587624855630620000008982042343058031036_" gradientUnits="userSpaceOnUse" x1="37.5" y1="635" x2="437.5" y2="635">
|
||||
<stop offset="0" style="stop-color:#844215"/>
|
||||
<stop offset="1" style="stop-color:#D38047"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000181052587624855630620000008982042343058031036_);" d="M108.5,357.5
|
||||
C104.2,385.9,82,550.1,195,691c76.7,95.6,174.6,129.4,213.5,140.5c9.7,27,19.3,54,29,81C400.4,906.1,246.9,875.2,137,733
|
||||
C48,617.8,38.8,495.9,37.5,452.5C61.2,420.8,84.8,389.2,108.5,357.5z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000124853241788307491970000002479847885029808270_" gradientUnits="userSpaceOnUse" x1="90" y1="683.9865" x2="418.2433" y2="683.9865">
|
||||
<stop offset="4.792333e-09" style="stop-color:#904C1D"/>
|
||||
<stop offset="1" style="stop-color:#7D3810"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000124853241788307491970000002479847885029808270_);" d="M408.5,831.5
|
||||
C373.5,822,287.9,793.9,214,713c-72.9-79.8-95.5-169.3-102-205c-8,19.7-14,42.3-22,62c15.8,39.8,51.8,115.1,128,183
|
||||
c77.8,69.3,158.7,96.2,200.2,107C415,850.5,411.7,841,408.5,831.5z"/>
|
||||
<path class="st36" d="M106.3,469l5.6,39.1c-5.9,14.1-11.6,29.4-16.9,46c-8.3,25.8-15,53.1-19,75c-3-7.4-6.1-15-9-23
|
||||
c-2.9-7.9-5.6-16.5-8-24c5.3-18.4,13.1-38.6,22-60C89.1,502.3,97.8,484.6,106.3,469z"/>
|
||||
<path class="st37" d="M145.3,609.6l17.4,33.5c-0.4,17.9-0.1,37.2,1.2,57.9c1.6,24.7,4.5,51.5,8,72c-6.1-5.9-12.5-12.5-19-20
|
||||
c-6-6.9-11.3-13.6-16-20c-0.8-16.8,0-36.3,1-56C139.2,652.3,141.9,629.7,145.3,609.6z"/>
|
||||
<path class="st38" d="M227.1,726.5l29.6,27c4.5,15.1,10.2,31.4,17.3,48.6c10.7,25.8,23.6,50.1,35,69c-7.6-3.5-15.7-7.5-24-12
|
||||
c-7.9-4.3-15.2-8.7-22-13c-6.6-17.1-13-36.4-19-56C237.1,767.7,231.6,746.5,227.1,726.5z"/>
|
||||
<path class="st39" d="M227.1,726.5c1.7,8.2,3.6,16.7,5.9,25.5c1.6,6.2,3.3,12.2,5.1,17.9c10.9,7.9,21.7,15.9,32.6,23.8l-13.9-40.8
|
||||
L227.1,726.5z"/>
|
||||
<path class="st39" d="M163.7,696l-24.8-33.5c0.5-7.5,1.2-15.4,2.1-23.5c1.2-10.3,2.7-20.1,4.3-29.4c5.8,11.2,11.6,22.3,17.4,33.5
|
||||
L163.7,696z"/>
|
||||
<path class="st40" d="M195.9,538.6c-12-6.9-25.1-15.2-38.8-25.3c-2.3-1.7-4.7-3.5-7-5.3c-17.4-13.4-31.9-26.8-43.7-39
|
||||
c-0.5,5.5-0.6,12.8,0.7,21c1.1,7.2,3,13.3,4.9,18c16.7,15.7,31.8,26.6,43.1,34c2.4,1.5,4.7,3,7.1,4.4
|
||||
c16.6,10.1,31.6,17.2,44.9,22.6C203.1,558.8,199.8,548.8,195.9,538.6z"/>
|
||||
<path class="st41" d="M251.5,643.1c-13.6-1.5-29.4-4.3-46.5-9.1c-24.2-6.9-44.2-15.9-59.7-24.4c1.2,4.5,3,9.7,5.7,15.4
|
||||
c3.7,7.7,8.1,13.8,11.8,18.1c4.7,2.1,11.7,5,20.2,7.9c12.6,4.3,22.5,6.4,30,8c25.7,5.4,49.6,9.8,63,12.2
|
||||
C267.8,661.9,259.7,652.5,251.5,643.1z"/>
|
||||
<path class="st42" d="M227.6,727.1c17,2.2,37,3.6,59.4,2.9c20.5-0.7,38.8-2.9,54.5-5.8c9.8,5.9,19.7,11.8,29.5,17.8
|
||||
c-12.7,4.3-29.9,6.3-47,9c-26.2,4.2-49.2,4-67.3,2.4c-4.6-2.7-10.1-6.4-15.7-11.4C235.2,736.8,230.8,731.5,227.6,727.1z"/>
|
||||
<polygon class="st43" points="192.4,630.1 209,658 276,671.2 251.5,643.1 "/>
|
||||
<polyline class="st44" points="208,572 162.1,546.4 157,513.3 195.9,538.6 208,572 "/>
|
||||
<path class="st45" d="M277,731l28.9,22.3c10.8-1.1,22.4-2.6,34.6-4.8c10.8-1.9,22-5.1,31.5-7.5c-9.8-5.9-20.7-10.8-30.5-16.8
|
||||
c-9.8,1.6-20.1,3.1-31,4.3C298.7,729.8,287.7,730.5,277,731z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st18" d="M627,741l-24.9,61.1l-30.4,10.9l-38.2-37.6c18-5.9,36.5-12.3,55.5-19.5C602.1,751.1,614.7,746.1,627,741z"/>
|
||||
<polygon class="st19" points="590.5,831.5 602.1,802.1 571.7,813.1 "/>
|
||||
<path class="st20" d="M693.1,753.3L627,741l-24.9,61.1c14.1-5.3,30-12.1,46.9-21.1C666.1,771.8,680.8,762.3,693.1,753.3z"/>
|
||||
<path class="st21" d="M742.3,753.4l-49.1-0.2c-12.9,9.1-28,18.6-45.1,27.7c-16.3,8.7-31.8,15.6-45.9,21.1
|
||||
c-3.9,9.8-7.7,19.6-11.6,29.4C612,825.8,638,817,666,803C698.1,787,723.5,769,742.3,753.4z"/>
|
||||
<path class="st22" d="M771.4,727.1l-19.8-22.9L722,731c8,0,16.9-0.2,26-1C756.3,729.3,764.1,728.3,771.4,727.1z"/>
|
||||
<path class="st23" d="M657.5,724.2c11.1,1.9,22.9,3.5,35.5,4.8c10.1,1,19.8,1.6,29,2c9.9-8.9,19.8-17.8,29.6-26.7l-28.6-33
|
||||
L657.5,724.2z"/>
|
||||
<polygon class="st24" points="790,658 751.6,704.3 723,671.2 "/>
|
||||
<path class="st25" d="M836.2,643.1c-7.5,2.7-15.2,5.3-23.2,7.9c-7.8,2.5-15.5,4.8-23,7c-12.8,15.4-25.6,30.8-38.4,46.3l19.8,22.9
|
||||
c11.7-11.2,24.7-25.2,37.6-42.1C820.2,670.2,829.1,655.9,836.2,643.1z"/>
|
||||
<path class="st26" d="M836.9,546.4c-1.8,7.3-3.7,14.8-5.9,22.6c-2.2,7.7-4.5,15.1-6.8,22.2c-10.9-6.5-22.2-12.6-33.2-19.2
|
||||
L836.9,546.4z"/>
|
||||
<path class="st27" d="M853.7,609.6l-29.5-18.5l-17.6,39c7.6-2.1,16.2-5,25.4-9.1C840.3,617.3,847.5,613.4,853.7,609.6z"/>
|
||||
<path class="st28" d="M824.2,591.2L791,572c-5.1,10.8-10.7,21.9-18,34c-8.5,14.1-17.2,26.5-25.5,37.1c19.7-4.3,39.4-8.7,59.1-13
|
||||
L824.2,591.2z"/>
|
||||
<path class="st28" d="M776,608"/>
|
||||
<path class="st29" d="M887.1,508c-6.2,5.6-12.9,11.3-20.1,17c-10.4,8.2-20.5,15.3-30.1,21.4c-4.3,14.9-8.5,29.8-12.8,44.7
|
||||
l29.5,18.5c6.9-13.2,14.1-29.1,20.3-47.6C880.7,542,884.7,523.6,887.1,508z"/>
|
||||
<polygon class="st30" points="840,430 820.5,452.5 842.5,456.2 "/>
|
||||
<path class="st31" d="M892.7,469l-50.2-12.8l-0.6,57.1c7.9-5.4,16.3-11.8,25-19.3C877,485.4,885.5,476.9,892.7,469z"/>
|
||||
<path class="st32" d="M890.5,357.5L840,430l2.5,26.2l50.2,12.8c1.7-16.9,2.7-36,2.3-57C894.5,391.9,892.8,373.7,890.5,357.5z"/>
|
||||
<path class="st33" d="M842.5,456.2l-22-3.7c-0.5,13-1.9,28.4-5.5,45.5c-3.3,15.6-7.5,29.2-11.9,40.6c12.9-8.4,25.9-16.9,38.8-25.3
|
||||
c0.6-9.6,1-19.7,1-30.3C843.1,473.7,842.9,464.8,842.5,456.2z"/>
|
||||
<path class="st25" d="M815,647"/>
|
||||
|
||||
<linearGradient id="SVGID_00000109747415768703344880000013296253376583759777_" gradientUnits="userSpaceOnUse" x1="267.5" y1="635" x2="667.5" y2="635" gradientTransform="matrix(-1 0 0 1 1229 0)">
|
||||
<stop offset="0" style="stop-color:#844215"/>
|
||||
<stop offset="1" style="stop-color:#D38047"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000109747415768703344880000013296253376583759777_);" d="M890.5,357.5
|
||||
C894.8,385.9,917,550.1,804,691c-76.7,95.6-174.6,129.4-213.5,140.5c-9.7,27-19.3,54-29,81c37.1-6.4,190.6-37.3,300.5-179.5
|
||||
c89-115.2,98.2-237.1,99.5-280.5C937.8,420.8,914.2,389.2,890.5,357.5z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000017506882430593231550000015389667641078630316_" gradientUnits="userSpaceOnUse" x1="320" y1="683.9865" x2="648.2432" y2="683.9865" gradientTransform="matrix(-1 0 0 1 1229 0)">
|
||||
<stop offset="4.792333e-09" style="stop-color:#904C1D"/>
|
||||
<stop offset="1" style="stop-color:#7D3810"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000017506882430593231550000015389667641078630316_);" d="M590.5,831.5
|
||||
c35-9.5,120.6-37.6,194.5-118.5c72.9-79.8,95.5-169.3,102-205c8,19.7,14,42.3,22,62c-15.8,39.8-51.8,115.1-128,183
|
||||
c-77.8,69.3-158.7,96.2-200.2,107C584,850.5,587.3,841,590.5,831.5z"/>
|
||||
<path class="st36" d="M892.7,469l-5.6,39.1c5.9,14.1,11.6,29.4,16.9,46c8.3,25.8,15,53.1,19,75c3-7.4,6.1-15,9-23
|
||||
c2.9-7.9,5.6-16.5,8-24c-5.3-18.4-13.1-38.6-22-60C909.9,502.3,901.2,484.6,892.7,469z"/>
|
||||
<path class="st37" d="M853.7,609.6l-17.4,33.5c0.4,17.9,0.1,37.2-1.2,57.9c-1.6,24.7-4.5,51.5-8,72c6.1-5.9,12.5-12.5,19-20
|
||||
c6-6.9,11.3-13.6,16-20c0.8-16.8,0-36.3-1-56C859.8,652.3,857.1,629.7,853.7,609.6z"/>
|
||||
<path class="st38" d="M771.9,726.5l-29.6,27c-4.5,15.1-10.2,31.4-17.3,48.6c-10.7,25.8-23.6,50.1-35,69c7.6-3.5,15.7-7.5,24-12
|
||||
c7.9-4.3,15.2-8.7,22-13c6.6-17.1,13-36.4,19-56C761.9,767.7,767.4,746.5,771.9,726.5z"/>
|
||||
<path class="st39" d="M771.9,726.5c-1.7,8.2-3.6,16.7-5.9,25.5c-1.6,6.2-3.3,12.2-5.1,17.9c-10.9,7.9-21.7,15.9-32.6,23.8
|
||||
l13.9-40.8L771.9,726.5z"/>
|
||||
<path class="st39" d="M835.3,696l24.8-33.5c-0.5-7.5-1.2-15.4-2.1-23.5c-1.2-10.3-2.7-20.1-4.3-29.4
|
||||
c-5.8,11.2-11.6,22.3-17.4,33.5L835.3,696z"/>
|
||||
<path class="st40" d="M803.1,538.6c12-6.9,25.1-15.2,38.8-25.3c2.3-1.7,4.7-3.5,7-5.3c17.4-13.4,31.9-26.8,43.7-39
|
||||
c0.5,5.5,0.6,12.8-0.7,21c-1.1,7.2-3,13.3-4.9,18c-16.7,15.7-31.8,26.6-43.1,34c-2.4,1.5-4.7,3-7.1,4.4
|
||||
c-16.6,10.1-31.6,17.2-44.9,22.6C795.9,558.8,799.2,548.8,803.1,538.6z"/>
|
||||
<path class="st41" d="M747.5,643.1c13.6-1.5,29.4-4.3,46.5-9.1c24.2-6.9,44.2-15.9,59.7-24.4c-1.2,4.5-3,9.7-5.7,15.4
|
||||
c-3.7,7.7-8.1,13.8-11.8,18.1c-4.7,2.1-11.7,5-20.2,7.9c-12.6,4.3-22.5,6.4-30,8c-25.7,5.4-49.6,9.8-63,12.2
|
||||
C731.2,661.9,739.3,652.5,747.5,643.1z"/>
|
||||
<path class="st42" d="M771.4,727.1c-17,2.2-37,3.6-59.4,2.9c-20.5-0.7-38.8-2.9-54.5-5.8c-9.8,5.9-19.7,11.8-29.5,17.8
|
||||
c12.7,4.3,29.9,6.3,47,9c26.2,4.2,49.2,4,67.3,2.4c4.6-2.7,10.1-6.4,15.7-11.4C763.8,736.8,768.2,731.5,771.4,727.1z"/>
|
||||
<polygon class="st43" points="806.6,630.1 790,658 723,671.2 747.5,643.1 "/>
|
||||
<polyline class="st44" points="791,572 836.9,546.4 842,513.3 803.1,538.6 791,572 "/>
|
||||
<path class="st45" d="M722,731l-28.9,22.3c-10.8-1.1-22.4-2.6-34.6-4.8c-10.8-1.9-22-5.1-31.5-7.5c9.8-5.9,20.7-10.8,30.5-16.8
|
||||
c9.8,1.6,20.1,3.1,31,4.3C700.3,729.8,711.3,730.5,722,731z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Layer_3">
|
||||
<polygon class="st48" points="241.5,347.5 500,229 500,646 310.5,588.5 "/>
|
||||
<polygon class="st49" points="758,347.5 499.5,229 499.5,646 689,588.5 "/>
|
||||
<path class="st50" d="M499.5,260L737,367.5c-7.6,25.8-17,56.8-28,92c-17.4,55.5-22.8,70.1-37,86c-4,4.5-16.7,18-61,39
|
||||
c-25.8,12.2-63.2,27.4-111.5,38.5C499.5,502,499.5,381,499.5,260z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000014612060945716269740000004954183021845470646_" gradientUnits="userSpaceOnUse" x1="381.25" y1="623" x2="381.25" y2="283.2944">
|
||||
<stop offset="9.244864e-02" style="stop-color:#C67121"/>
|
||||
<stop offset="1" style="stop-color:#FFCE48"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000014612060945716269740000004954183021845470646_);" d="M500,260L262.5,367.5
|
||||
c7.6,25.8,17,56.8,28,92c17.4,55.5,22.8,70.1,37,86c4,4.5,16.7,18,61,39c25.8,12.2,63.2,27.4,111.5,38.5C500,502,500,381,500,260z"
|
||||
/>
|
||||
<polygon class="st52" points="301.5,388.5 341.5,523.5 500,523.5 500,320 421.5,427.5 "/>
|
||||
<polygon class="st53" points="341.5,523.5 500,579 500,523.5 "/>
|
||||
<polygon class="st54" points="658,523.5 499.5,579 499.5,523.5 "/>
|
||||
<polygon class="st0" points="301.5,388.5 426.5,462.5 500,320 421.5,427.5 "/>
|
||||
<polygon class="st55" points="698,388.5 658,523.5 499.5,523.5 499.5,320 578,427.5 "/>
|
||||
<polygon class="st56" points="698,388.5 573,462.5 499.5,320 578,427.5 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
40
assets/badges/developer.svg
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="343"
|
||||
height="343"
|
||||
viewBox="0 0 343 343"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="developer.svg"
|
||||
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.5451895"
|
||||
inkscape:cx="171.30355"
|
||||
inkscape:cy="171.5"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2089"
|
||||
inkscape:window-x="1680"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M 305.64988,87.191016 247.50845,145.3188 194.6717,92.467053 252.79949,34.340626 C 217.61455,19.871627 175.68861,26.897158 147.12599,55.489784 118.56337,84.052402 111.52229,125.99334 125.99184,161.17828 l -93.806513,93.79014 c -5.84347,5.84347 -5.84347,15.29069 0,21.13416 l 31.7176,31.7176 c 5.84347,5.84347 15.290685,5.84347 21.134154,0 l 93.790139,-93.80651 c 35.18495,14.46818 77.12589,7.42846 105.68851,-21.13416 28.56262,-28.57762 35.58869,-70.51856 21.13415,-105.688494 z"
|
||||
fill="#99aab5"
|
||||
id="path4"
|
||||
style="stroke-width:1.36402" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
4
assets/badges/early_adopter.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="343" height="343" viewBox="0 0 343 343" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M252.571 30L333.429 171.5L252.571 313H90.8571L10 171.5L90.8571 30H252.571Z" fill="#509AF0"/>
|
||||
<path d="M164.486 82.3018C182.31 82.3018 196.421 86.5928 206.818 95.1748C217.298 103.757 222.538 115.722 222.538 131.071C222.538 139.323 220.475 146.791 216.349 153.475C212.223 160.159 206.406 165.523 198.896 169.566C209.046 173.032 216.803 178.561 222.167 186.153C227.613 193.744 230.336 202.945 230.336 213.755C230.336 230.672 225.427 243.875 215.607 253.365C205.787 262.772 192.295 267.476 175.131 267.476C162.093 267.476 150.499 264.34 140.349 258.068V313H104.577V137.012C104.577 126.697 107.176 117.373 112.375 109.038C117.573 100.621 124.794 94.0608 134.036 89.3572C143.278 84.6536 153.428 82.3018 164.486 82.3018ZM186.766 133.794C186.766 127.027 184.703 121.581 180.577 117.455C176.534 113.329 171.17 111.266 164.486 111.266C157.472 111.266 151.695 113.618 147.157 118.322C142.618 122.943 140.349 129.338 140.349 137.507V231.456C147.115 236.242 156.028 238.635 167.085 238.635C175.502 238.635 182.186 236.283 187.137 231.58C192.089 226.793 194.564 220.687 194.564 213.26C194.564 204.348 192.295 197.416 187.756 192.465C183.3 187.432 176.699 184.915 167.952 184.915H155.945V158.797H165.6C179.711 158.302 186.766 149.968 186.766 133.794Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
41
assets/badges/founder.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="343"
|
||||
height="343"
|
||||
viewBox="0 0 343 343"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="founder.svg"
|
||||
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.5451895"
|
||||
inkscape:cx="171.30355"
|
||||
inkscape:cy="171.5"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2089"
|
||||
inkscape:window-x="1680"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="m 233.20363,120.30495 c 0,23.32978 -12.69345,37.33415 -39.78401,37.33415 h -44.853 V 83.816247 h 44.86112 c 27.08244,0 39.77589,14.422841 39.77589,36.488703 z M 32.781712,23.579841 76.206371,83.95858 V 321.35524 H 148.57475 V 208.96003 h 17.35345 l 61.7961,112.41959 h 81.68066 L 240.84435,203.44414 c 19.14242,-4.66948 36.11939,-15.75136 48.12217,-31.41064 12.00278,-15.65927 18.31226,-34.95744 17.88296,-54.70047 0,-51.756865 -36.39295,-93.753189 -109.18833,-93.753189 H 76.206371 Z"
|
||||
fill="#efab44"
|
||||
stroke="#efab44"
|
||||
stroke-width="0.97733"
|
||||
id="path2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
154
assets/badges/moderation.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
38
assets/badges/paw.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<metadata>
|
||||
<rdf:RDF xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:dc = "http://purl.org/dc/elements/1.1/"
|
||||
>
|
||||
|
||||
<rdf:Description rdf:about="">
|
||||
<dc:title>Mutant Standard emoji 2020.04</dc:title>
|
||||
</rdf:Description>
|
||||
|
||||
<cc:work rdf:about="">
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/"/>
|
||||
<cc:attributionName>Dzuk</cc:attributionName>
|
||||
<cc:attributionURL>http://mutant.tech/</cc:attributionURL>
|
||||
</cc:work>
|
||||
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
|
||||
<rect id="v--paw-" serif:id="v [paw]" x="0" y="0" width="32" height="32" style="fill:none;"/>
|
||||
<clipPath id="_clip1">
|
||||
<rect x="0" y="0" width="32" height="32"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<g id="Layer10">
|
||||
<path d="M19.25,3.52c0.755,-2.176 3.064,-3.455 5.341,-2.886c2.41,0.603 3.877,3.048 3.275,5.457l-1.866,7.463l0,1.77c0.832,0.315 1.575,0.85 2.142,1.563c0.948,1.193 1.301,2.753 0.959,4.237c-0.116,0.502 -0.238,1.032 -0.363,1.574c-1.257,5.445 -6.105,9.302 -11.693,9.302l-0.045,-0.018l0,0.014c0,0 -0.412,0 -1.035,0c-5.571,0 -10.426,-3.79 -11.778,-9.194c-0.609,-2.431 -1.178,-4.706 -1.178,-4.706c-0.603,-2.409 0.864,-4.855 3.273,-5.457c0.165,-0.042 0.331,-0.073 0.495,-0.096c0.188,-1.832 1.498,-3.436 3.387,-3.909c0.358,-0.089 0.718,-0.133 1.072,-0.135l-0.602,-2.408c-0.602,-2.409 0.865,-4.854 3.275,-5.457c2.277,-0.569 4.586,0.71 5.341,2.886Z"/>
|
||||
</g>
|
||||
<g id="Layer9">
|
||||
<path d="M7,15c2.268,-2.268 2.496,-1.997 5,-4c0.736,-0.589 1.057,-2 2,-2c0.943,0 1.333,1.333 2,2l8,4l0,9l-8,-2l-9,0l0,-1.606c-0.517,-0.015 -1.015,-0.156 -1.455,-0.402l-0.558,-2.229l2.013,-2.763Z" style="fill:#5E3583;"/>
|
||||
<path d="M24.229,17c0.914,0 1.778,0.417 2.348,1.132c0.569,0.716 0.781,1.652 0.575,2.543c-0.116,0.502 -0.238,1.032 -0.363,1.574c-1.047,4.537 -5.088,7.751 -9.744,7.751l-0.045,0c0.001,-0.001 0.001,-0.003 0.002,-0.004l-0.002,0l-1.035,0c-4.653,0 -8.709,-3.166 -9.838,-7.679l-0.582,-2.325c0.681,0.381 1.503,0.511 2.316,0.308c0.01,-0.002 0.021,-0.005 0.031,-0.008c0.933,-0.233 1.709,-0.877 2.111,-1.75c0.138,-0.299 0.226,-0.614 0.266,-0.933c0.694,0.426 1.549,0.577 2.385,0.368c0.001,0 0.002,-0.001 0.002,-0.001c0.201,-0.05 0.393,-0.119 0.574,-0.205c-0.02,0.019 0.978,0.078 0.909,-0.655c0.538,-0.562 0.861,-1.321 0.861,-2.141c0,-0.161 0,-0.313 0,-0.447c0,-0.405 -0.068,-0.807 -0.2,-1.19l-1.059,-3.064l-1.166,-4.668c-0.335,-1.338 0.48,-2.697 1.819,-3.031c1.338,-0.335 2.697,0.48 3.031,1.819l1.825,7.298l1.825,-7.298c0.088,-0.344 0.243,-0.666 0.463,-0.944c0.2,-0.252 0.448,-0.465 0.728,-0.624c0.271,-0.154 0.57,-0.257 0.879,-0.301c0.277,-0.04 0.561,-0.032 0.836,0.022c0.269,0.052 0.529,0.149 0.766,0.286c0.22,0.126 0.42,0.286 0.591,0.473c0.347,0.377 0.574,0.86 0.641,1.369c0.041,0.312 0.02,0.627 -0.054,0.931l-1.925,7.702l0,1.692l-8,0l0,1.663l-1,1.337c0,3.314 3.686,6 7,6l1,-1l0,-2l-1,0c-2.209,0 -4,-1.791 -4,-4l6.229,0Zm-19.242,0.763l-0.038,-0.152c-0.335,-1.339 0.48,-2.697 1.818,-3.032c0.521,-0.131 1.045,-0.087 1.511,0.094l0.786,1.573c0.295,0.589 0.306,1.28 0.031,1.878c-0.275,0.598 -0.807,1.039 -1.446,1.198c-0.01,0.003 -0.02,0.005 -0.031,0.008c-1.119,0.28 -2.262,-0.365 -2.6,-1.468l-0.031,-0.099Zm3.816,-4.275c-0.26,-1.299 0.546,-2.589 1.846,-2.913c0.876,-0.219 1.761,0.054 2.361,0.645l0.845,2.444c0.096,0.278 0.145,0.57 0.145,0.864c0,0.134 0,0.286 0,0.447c0,0.961 -0.654,1.798 -1.586,2.031c-0.001,0 -0.002,0.001 -0.003,0.001c-0.96,0.24 -1.958,-0.219 -2.401,-1.105l-1.207,-2.414Z" style="fill:#C596FD;"/>
|
||||
<path d="M23,23l0,1.158c0,0.923 -0.426,1.796 -1.154,2.364c-0.728,0.569 -1.677,0.77 -2.574,0.546c-0.917,-0.229 -1.782,-0.445 -2.294,-0.574c-0.318,-0.079 -0.643,-0.119 -0.97,-0.119l-0.016,0c-0.327,0 -0.652,0.04 -0.97,0.119c-0.512,0.129 -1.377,0.345 -2.294,0.574c-0.897,0.224 -1.846,0.023 -2.574,-0.546c-0.728,-0.568 -1.154,-1.441 -1.154,-2.364c0,-0.172 0,-0.34 0,-0.501c0,-1.061 0.421,-2.078 1.172,-2.829c0.819,-0.819 1.951,-1.951 3,-3c0.019,-0.019 0.038,-0.038 0.058,-0.057c0.345,-0.163 0.652,-0.386 0.909,-0.655c0.569,-0.299 1.207,-0.459 1.861,-0.459l0,0.343c0,3.314 2.686,6 6,6l1,0Z" style="fill:#8149BC;"/>
|
||||
<path d="M23.704,4.041c1.047,0.261 1.589,1.707 1.21,3.226c-0.378,1.52 -1.536,2.542 -2.583,2.281c-1.047,-0.261 -1.589,-1.707 -1.21,-3.227c0.379,-1.519 1.536,-2.541 2.583,-2.28Zm-8.943,0.001c-1.047,0.261 -1.596,1.677 -1.227,3.16c0.37,1.483 1.52,2.475 2.567,2.214c1.047,-0.261 1.597,-1.677 1.227,-3.16c-0.37,-1.483 -1.52,-2.475 -2.567,-2.214Z" style="fill:#8149BC;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
20
assets/badges/raccoon.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:3;">
|
||||
<metadata>
|
||||
<rdf:RDF xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:dc = "http://purl.org/dc/elements/1.1/"
|
||||
>
|
||||
|
||||
<rdf:Description rdf:about="">
|
||||
<dc:title>Mutant Standard emoji 2020.04</dc:title>
|
||||
</rdf:Description>
|
||||
|
||||
<cc:work rdf:about="">
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/"/>
|
||||
<cc:attributionName>Dzuk</cc:attributionName>
|
||||
<cc:attributionURL>http://mutant.tech/</cc:attributionURL>
|
||||
</cc:work>
|
||||
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect id="raccoon" x="0" y="0" width="32" height="32" style="fill:none;"/><clipPath id="_clip1"><rect x="0" y="0" width="32" height="32"/></clipPath><g clip-path="url(#_clip1)"><g id="outline"><path d="M21.369,8.952c0.087,-0.295 0.247,-0.568 0.471,-0.792c0.001,-0.001 0.001,-0.001 0.002,-0.002c0.105,-0.105 0.226,-0.192 0.358,-0.258c0.792,-0.396 3.729,-1.865 5.094,-2.547c0.187,-0.094 0.404,-0.109 0.603,-0.043c0.198,0.066 0.362,0.209 0.456,0.396c0,0 0,0 0,0.001c0.407,0.814 0.407,1.772 0,2.586c-0.575,1.151 -1.401,2.803 -1.906,3.813c-0.009,0.017 -0.018,0.034 -0.027,0.051c0.715,0.946 1.222,2.054 1.462,3.255c0.536,2.68 1.118,5.588 1.118,5.588l-2,0l0,2l-2,-1c0,0 -3.283,2.189 -4.992,3.328c-0.657,0.438 -1.429,0.672 -2.219,0.672c-1.042,0 -2.536,0 -3.578,0c-0.79,0 -1.562,-0.234 -2.219,-0.672c-1.709,-1.139 -4.992,-3.328 -4.992,-3.328l-2,1l0,-2l-2,0l1.118,-5.588c0.24,-1.201 0.747,-2.309 1.462,-3.255c-0.009,-0.017 -0.018,-0.034 -0.027,-0.051c-0.505,-1.01 -1.331,-2.662 -1.906,-3.813c-0.407,-0.814 -0.407,-1.772 0,-2.586c0,-0.001 0,-0.001 0,-0.001c0.094,-0.187 0.258,-0.33 0.456,-0.396c0.199,-0.066 0.416,-0.051 0.603,0.043c1.365,0.682 4.302,2.151 5.094,2.547c0.132,0.066 0.253,0.153 0.358,0.258c0.001,0.001 0.001,0.001 0.002,0.002c0.224,0.224 0.384,0.497 0.471,0.792l1.268,-0.461c2.649,-0.963 5.553,-0.963 8.202,0l1.268,0.461Z" style="fill:none;stroke:#000;stroke-width:4px;"/></g><g id="emoji"><g><path d="M3.993,16.036l0.125,-0.624c0.548,-2.74 2.485,-4.995 5.11,-5.95c0.877,-0.318 1.799,-0.654 2.671,-0.971c2.649,-0.963 5.553,-0.963 8.202,0c0.872,0.317 1.794,0.653 2.671,0.971c2.625,0.955 4.562,3.21 5.11,5.95l0.125,0.624c-2.191,-1.95 -5.028,-3.036 -7.978,-3.036c-0.009,0 -0.019,0 -0.029,0c-1.657,0 -3,1.343 -3,3c0,1.105 0.895,2 2,2c0,0 0,0 0,0l1,1l-4,4l-4,-4l1,-1l0,0c1.105,0 2,-0.895 2,-2c0,-1.657 -1.343,-3 -3,-3c-0.01,0 -0.02,0 -0.029,0c-2.95,0 -5.787,1.086 -7.978,3.036Z" style="fill:#878787;"/><path d="M19.333,19.667c0.01,0.004 3.479,0.333 3.479,0.333c0,0 -0.353,2.537 0,3.459l-2.804,1.869c-0.657,0.438 -1.429,0.672 -2.219,0.672c-1.042,0 -2.536,0 -3.578,0c-0.79,0 -1.562,-0.234 -2.219,-0.672l-2.804,-1.869c0.342,-0.893 0,-3.097 0,-3.097c0,0 3.469,-0.691 3.479,-0.695l3.333,3.333l3.333,-3.333Zm-1.319,-1.927c-0.606,-0.343 -1.014,-0.994 -1.014,-1.74c0,-1.657 1.343,-3 3,-3c0.01,0 0.02,0 0.029,0c2.95,0 5.787,1.086 7.978,3.036l0.993,4.964l-1.667,0c0,0 -9.328,-3.346 -9.319,-3.26Zm-13.347,3.26l-1.667,0l0.993,-4.964c2.191,-1.95 5.028,-3.036 7.978,-3.036c0.009,0 0.019,0 0.029,0c1.657,0 3,1.343 3,3c0,0.746 -0.408,1.397 -1.014,1.74c0.009,-0.086 -9.319,3.26 -9.319,3.26Z" style="fill:#e1e1e1;"/><path d="M4.667,21l1.626,-2.927c1.054,-1.897 3.053,-3.073 5.222,-3.073c0.003,0 0.006,0 0.009,0c1.367,0 2.476,1.109 2.476,2.476c0,0.001 0,0.002 0,0.002c0,0.933 -0.527,1.785 -1.361,2.202c-0.015,0.008 -0.03,0.016 -0.046,0.023c-1.567,0.784 -2.781,2.126 -3.405,3.756l-2.188,-1.459l-2,1l0,-2l-0.333,0Zm18.145,2.459c-0.624,-1.63 -1.838,-2.972 -3.405,-3.756c-0.016,-0.007 -0.031,-0.015 -0.046,-0.023c-0.834,-0.417 -1.361,-1.269 -1.361,-2.202c0,0 0,-0.001 0,-0.002c0,-1.367 1.109,-2.476 2.476,-2.476c0.003,0 0.006,0 0.009,0c2.169,0 4.168,1.176 5.222,3.073l1.626,2.927l-0.333,0l0,2l-2,-1l-2.188,1.459Z" style="fill:#484848;"/><g><circle cx="11.5" cy="17.5" r="1.5"/><circle cx="20.5" cy="17.5" r="1.5"/><path d="M15.236,21c-0.155,0 -0.308,0.036 -0.447,0.106c-0.047,0.023 -0.1,0.049 -0.158,0.078c-0.901,0.451 -1.266,1.546 -0.815,2.447c0,0.001 0,0.001 0,0.001c0.113,0.226 0.343,0.368 0.595,0.368c0.768,0 2.41,0 3.178,0c0.252,0 0.482,-0.142 0.595,-0.368c0,0 0,0 0,-0.001c0.451,-0.901 0.086,-1.996 -0.815,-2.447c-0.058,-0.029 -0.111,-0.055 -0.158,-0.078c-0.139,-0.07 -0.292,-0.106 -0.447,-0.106c-0.385,0 -1.143,0 -1.528,0Z"/></g></g><path d="M6.894,12.553c-0.494,0.247 -1.094,0.047 -1.341,-0.447c-0.505,-1.01 -1.331,-2.662 -1.906,-3.813c-0.407,-0.814 -0.407,-1.772 0,-2.586c0,-0.001 0,-0.001 0,-0.001c0.094,-0.187 0.258,-0.33 0.456,-0.396c0.199,-0.066 0.416,-0.051 0.603,0.043c1.365,0.682 4.302,2.151 5.094,2.547c0.132,0.066 0.253,0.153 0.358,0.258c0.001,0.001 0.001,0.001 0.002,0.002c0.426,0.426 0.621,1.031 0.525,1.627c-0.097,0.595 -0.474,1.107 -1.013,1.377c-0.973,0.486 -2.044,1.022 -2.778,1.389Zm18.212,0c0.494,0.247 1.094,0.047 1.341,-0.447c0.505,-1.01 1.331,-2.662 1.906,-3.813c0.407,-0.814 0.407,-1.772 0,-2.586c0,-0.001 0,-0.001 0,-0.001c-0.094,-0.187 -0.258,-0.33 -0.456,-0.396c-0.199,-0.066 -0.416,-0.051 -0.603,0.043c-1.365,0.682 -4.302,2.151 -5.094,2.547c-0.132,0.066 -0.253,0.153 -0.358,0.258c-0.001,0.001 -0.001,0.001 -0.002,0.002c-0.426,0.426 -0.621,1.031 -0.525,1.627c0.097,0.595 0.474,1.107 1.013,1.377c0.973,0.486 2.044,1.022 2.778,1.389Z" style="fill:#484848;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
41
assets/badges/revolt_r.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="343"
|
||||
height="343"
|
||||
viewBox="0 0 343 343"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="founder.svg"
|
||||
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.5451895"
|
||||
inkscape:cx="171.30355"
|
||||
inkscape:cy="171.5"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2089"
|
||||
inkscape:window-x="1680"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="m 233.20363,120.30495 c 0,23.32978 -12.69345,37.33415 -39.78401,37.33415 h -44.853 V 83.816247 h 44.86112 c 27.08244,0 39.77589,14.422841 39.77589,36.488703 z M 32.781712,23.579841 76.206371,83.95858 V 321.35524 H 148.57475 V 208.96003 h 17.35345 l 61.7961,112.41959 h 81.68066 L 240.84435,203.44414 c 19.14242,-4.66948 36.11939,-15.75136 48.12217,-31.41064 12.00278,-15.65927 18.31226,-34.95744 17.88296,-54.70047 0,-51.756865 -36.39295,-93.753189 -109.18833,-93.753189 H 76.206371 Z"
|
||||
fill="#efab44"
|
||||
stroke="#efab44"
|
||||
stroke-width="0.97733"
|
||||
id="path2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
153
assets/badges/supporter.svg
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"
|
||||
id="svg46"
|
||||
sodipodi:docname="supporter.svg"
|
||||
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||
id="defs50" /><sodipodi:namedview
|
||||
id="namedview48"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="27.28125"
|
||||
inkscape:cx="15.981672"
|
||||
inkscape:cy="16"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2089"
|
||||
inkscape:window-x="1680"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg46" />
|
||||
<metadata
|
||||
id="metadata2">
|
||||
<rdf:RDF>
|
||||
|
||||
<rdf:Description
|
||||
rdf:about="">
|
||||
<dc:title>Mutant Standard emoji 2020.04</dc:title>
|
||||
</rdf:Description>
|
||||
|
||||
<cc:work
|
||||
rdf:about="">
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" />
|
||||
<cc:attributionName>Dzuk</cc:attributionName>
|
||||
<cc:attributionURL>http://mutant.tech/</cc:attributionURL>
|
||||
</cc:work>
|
||||
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
|
||||
<rect
|
||||
id="green_money"
|
||||
x="0"
|
||||
y="0"
|
||||
width="32"
|
||||
height="32"
|
||||
style="fill:none;" />
|
||||
|
||||
<g
|
||||
id="Layer2">
|
||||
<path
|
||||
d="M29,21l-26,0l-1,1l0,3l28,0l0,-3l-1,-1Z"
|
||||
style="fill:#60ae33;"
|
||||
id="path8" />
|
||||
<rect
|
||||
x="2"
|
||||
y="7"
|
||||
width="28"
|
||||
height="15"
|
||||
style="fill:#80c95b;"
|
||||
id="rect10" />
|
||||
<g
|
||||
id="g24">
|
||||
<path
|
||||
d="M8.5,9.5c0,-0.828 -0.672,-1.5 -1.5,-1.5l-2.5,0c-0.398,0 -0.779,0.158 -1.061,0.439c-0.281,0.282 -0.439,0.663 -0.439,1.061c0,0.828 0.672,1.5 1.5,1.5l2.5,0c0.828,0 1.5,-0.672 1.5,-1.5l0,0Z"
|
||||
style="fill:#5da436;"
|
||||
id="path12" />
|
||||
<path
|
||||
d="M8.5,19.5c0,-0.828 -0.672,-1.5 -1.5,-1.5l-2.5,0c-0.398,0 -0.779,0.158 -1.061,0.439c-0.281,0.282 -0.439,0.663 -0.439,1.061c0,0.828 0.672,1.5 1.5,1.5l2.5,0c0.828,0 1.5,-0.672 1.5,-1.5l0,0Z"
|
||||
style="fill:#5da436;"
|
||||
id="path14" />
|
||||
<path
|
||||
d="M29,9.5c0,-0.828 -0.672,-1.5 -1.5,-1.5l-2.5,0c-0.398,0 -0.779,0.158 -1.061,0.439c-0.281,0.282 -0.439,0.663 -0.439,1.061c0,0.828 0.672,1.5 1.5,1.5l2.5,0c0.828,0 1.5,-0.672 1.5,-1.5l0,0Z"
|
||||
style="fill:#5da436;"
|
||||
id="path16" />
|
||||
<path
|
||||
d="M29,19.5c0,-0.828 -0.672,-1.5 -1.5,-1.5l-2.5,0c-0.398,0 -0.779,0.158 -1.061,0.439c-0.281,0.282 -0.439,0.663 -0.439,1.061c0,0.828 0.672,1.5 1.5,1.5l2.5,0c0.828,0 1.5,-0.672 1.5,-1.5l0,0Z"
|
||||
style="fill:#5da436;"
|
||||
id="path18" />
|
||||
<path
|
||||
d="M21,14c0,-0.552 -0.448,-1 -1,-1c-0.552,0 -1,0.448 -1,1c0,0.322 0,0.678 0,1c0,0.552 0.448,1 1,1c0.552,0 1,-0.448 1,-1c0,-0.322 0,-0.678 0,-1Z"
|
||||
style="fill:#5da436;"
|
||||
id="path20" />
|
||||
<path
|
||||
d="M24,14c0,-0.552 -0.448,-1 -1,-1c-0.552,0 -1,0.448 -1,1c0,0.322 0,0.678 0,1c0,0.552 0.448,1 1,1c0.552,0 1,-0.448 1,-1c0,-0.322 0,-0.678 0,-1Z"
|
||||
style="fill:#5da436;"
|
||||
id="path22" />
|
||||
</g>
|
||||
<rect
|
||||
x="8"
|
||||
y="11"
|
||||
width="8"
|
||||
height="7"
|
||||
style="fill:none;"
|
||||
id="rect26" />
|
||||
<clipPath
|
||||
id="_clip1">
|
||||
<rect
|
||||
x="8"
|
||||
y="11"
|
||||
width="8"
|
||||
height="7"
|
||||
id="rect28" />
|
||||
</clipPath>
|
||||
<g
|
||||
clip-path="url(#_clip1)"
|
||||
id="g37">
|
||||
<path
|
||||
d="M16,14l-8,0l0,-1l8,0l0,1Z"
|
||||
style="fill:#fff;"
|
||||
id="path31" />
|
||||
<path
|
||||
d="M16,16l-8,0l0,-1l8,0l0,1Z"
|
||||
style="fill:#fff;"
|
||||
id="path33" />
|
||||
<path
|
||||
d="M15,10.335l0,7.665l-1,0l0,-4.5l-1.771,4.25l-0.458,0l-1.771,-4.25l0,4.5l-1,0l0,-7.665l0.706,-0.141l2.294,5.506l2.294,-5.506l0.706,0.141Z"
|
||||
style="fill:#fff;"
|
||||
id="path35" />
|
||||
</g>
|
||||
<path
|
||||
d="M27,20l-22,0l0,-11l22,0l0,11Zm-21,-10l0,9l20,0l0,-9l-20,0Z"
|
||||
style="fill:#5da436;"
|
||||
id="path39" />
|
||||
<path
|
||||
d="M20,22l-1,-1l-6,0l-1,1l0,3l8,0l0,-3Z"
|
||||
style="fill:#ccc;"
|
||||
id="path41" />
|
||||
<rect
|
||||
x="12"
|
||||
y="7"
|
||||
width="8"
|
||||
height="15"
|
||||
style="fill:#eee;"
|
||||
id="rect43" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
15
assets/badges/translator.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg width="343" height="343" viewBox="0 0 343 343" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<rect x="141.421" y="31" width="200" height="200" transform="rotate(45 141.421 31)" fill="#01BE6E"/>
|
||||
<path d="M137.926 182.464C128.751 173.507 120.668 165.642 116.08 147.51H148.194V133.965H116.298V116.051H102.535V134.184H70.4214V147.728H103.191C103.191 147.728 102.972 150.35 102.535 152.316C97.9475 170.23 92.486 181.59 70.4214 192.731L75.0091 206.276C95.9814 195.134 106.904 181.153 111.711 165.642C116.298 177.439 124.163 187.051 133.12 195.79L137.926 182.464Z" fill="white"/>
|
||||
<path d="M180.307 138.551H161.956L129.842 228.775H143.605L152.781 201.686H189.482L198.658 228.775H212.421L180.307 138.551ZM157.368 188.142L171.132 152.095L184.895 188.36L157.368 188.142Z" fill="white"/>
|
||||
<path d="M305.473 170.182L208.174 267.48C206.834 268.82 205.148 269.773 203.306 270.234L148.421 281.018L159.206 226.123C159.666 224.291 160.619 222.605 161.959 221.265L259.258 123.967L274.421 108.803L320.421 103.921V155.233L305.473 170.182Z" fill="#99AAB5"/>
|
||||
<path d="M208.174 267.478L305.473 170.18L259.258 123.965L161.959 221.263C160.619 222.603 159.666 224.289 159.206 226.121L148.421 281.016L203.306 270.232C205.148 269.771 206.834 268.818 208.174 267.478ZM336.883 138.769C345.06 130.592 345.06 117.337 336.883 109.16L320.278 92.5542C312.1 84.3771 298.845 84.3771 290.668 92.5542L274.063 109.16L320.278 155.375L336.883 138.769Z" fill="#EA596E"/>
|
||||
<path d="M305.473 170.182L208.174 267.48C206.834 268.82 205.148 269.773 203.306 270.234L148.421 281.018L159.206 226.123C159.666 224.291 160.619 222.605 161.959 221.265L259.258 123.967L305.473 170.182Z" fill="#FFCC4D"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="343" height="343" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
assets/badges/verified.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1813.46 1814.1"><path d="M921.08.24H999.8L1006,2.16c6.23,1.17,13.75-1.26,19.44.48a5.52,5.52,0,0,0,2.16,3.12c3.16,1.43,7.67.35,11.52,1.44,2.09.59,3.31,2.7,5,3.36l7,1c2.8.67,4.61,2.41,6.72,3.36l7.2,1.2c12.55,5,24.78,11.55,37.2,16.8,1.89.8,3,3.13,4.56,3.84l4.08.24,3.84,4.08L1120,43.2c2.74,1.66,5,4.77,7.68,6.24l5,1.68c8.83,6.07,16.73,13.82,25,20.4,8.67,6.93,17.76,14.41,24.72,23l5.76,6,.24,7.68,2.64-2.64,1.2-1,7.68,7.44,48,49.2c5.63,5.63,9.25,12.8,19,14.4h41.28c10.65,0,23.19.31,32.64-1.2l35.52.24c9.24-1.38,25.9-2,35-.24l12,2.88,16.32.48,8.64,3.6,11.28.24,5.76,3.6,11,.48,3.6,3.12c7.54,3.39,18,3.14,22.32,9.6a54.24,54.24,0,0,1,8.4.24l1.68,6.24v-.48l.72-2.16,6.72,1,1.68,3.12,6.24.72,1,2.88,7,1.44a6.7,6.7,0,0,0,1.44,3.84,11.63,11.63,0,0,1,5.28.72c1.6,3.8,3.39,2.21,7,4.08,2.53,1.33,6.21,6.69,8.64,7.92l3.6.48,2.64,3.12c3,2.27,5.63,4.58,8.64,6.72v16.56l9.36-9.84,4.08,4.08c4.5,2.2,8.94,8.09,12.24,11.76,5.58,6.21,12.5,11.46,17.76,18l9.6,13.44c8.15,10.51,16.51,21.35,23.52,33.12l1.92,5.76c3.68,5.82,7.9,12.21,10.8,19l1.44,5.28,3.12,3.6.72,5,3.36,5.28c2.6,6.5,3.2,11.73,5.52,18.48l2.88,5.52,1,8.16,3.12,8.64,1.2,10.8c.76,2.59,2.3,5,2.88,8.16l.48,11.76,3.36,23.52v32.16c0,13.43,1.62,29.68-.48,42.24v30c-1.15,7.43-1.24,28.83.48,35.28,1.51,5.68,20.48,21.44,25.44,26.4L1786,699.12c11.59,14.43,24.59,28.4,34.56,44.4,15.84,25.41,27.31,54.64,37.2,85.92l1,9.6,3.84,10.32.48,13.2,3.36,15.36.24,21.12-.24,38.88-3.12,14.16-1.2,14.64c-10.22,47.84-30.48,87.55-54.72,121.44-5.68,7.94-11.74,15.07-17.76,22.56l-10.56,10.8-7.2,7.92-7.68,4.56-16.8,17c-2.39,2.39-6.08,4.6-8.16,7.2l-3.84,6.72-20.88,20.88-18,17.76c-3.08,2.3-5.81,4.45-7.44,8.16-1.8,4.09-1.2,11.21-1.2,17l-.24,26.16.24,49.2,3.12,6.72-.24,41.28c-7.07,5.4-1.08,14.6-2.88,25.2a265.49,265.49,0,0,1-7.44,32.88l-2.16,11c-8,20.27-14.89,40.47-24.72,58.8-12.75,23.77-30.76,43.38-47,63.6-8.91,11.07-21.32,20.11-32.64,28.8-33.16,25.46-71.21,45.91-118.08,57.84l-9.36,1-11,3.12c-11.56,2.36-24.16,1.72-35.52,3.6l-135.84.48c-8,2-17.66,15-23.28,20.64l-53.28,54c-33.3,35.63-74.19,65.09-124.56,83.52l-6.24,1.2c-6,2.07-13.87,5.06-20.16,7l-8.64,1.2-10.32,3.36-15.84,1-6.72,2.16c-8.44,1.87-30.53,2.58-40.32,1l-22.32.24c-5.44-.89-10.21-1.79-14.64-2.88-5.62-1.38-12.46-.12-17.52-1.44-43.65-11.4-82-25.84-114.24-48.48-13.42-9.43-24.89-20.61-37.44-30.72l-65.52-66-12.48-12.48c-3.32-4.12-8.24-10.48-13.44-12.48-7.2-2.77-28.85-1-38.64-1H514.28l-12.48-2.16c-1.4-.21-2.79,1.05-3.12,1l-6.72-1.92-7.92-.24-10.32-3.12-10.8-1.2c-32.62-8.51-66.3-22-91.44-38.4-9.39-6.13-18.22-13.61-27.12-20.16-11-8.11-23.15-17.25-31.68-27.84-17.47-21.68-34.42-40.92-47.76-67.2-3-5.82-6.17-12-9.36-18.24l-3.12-4.56-.24-4.32-3.36-5.52c-1.75-4.37-3.31-8.5-4.56-12.48-4.71-15-8.92-31.62-12.24-47.76-1.1-5.35-2.41-13-1.2-19.44.4-2.13,2-6.72.72-9.36l-3.12-2.88v-6.24c-1.3-8-1.64-20.48,0-28.32l.48-75.84c0-10.11,1.78-24.72-1.44-32.4-1.44-3.43-5.18-5.9-7.68-8.4L202,1183.68l-51.6-50.88c-6.65-5.35-12.9-12.75-18.24-19.44-7.9-9.9-17-19-24.48-29.52C84.53,1051.17,67.63,1011.91,56.84,967l-.72-13.68-2.88-11.52v-25.2c0-19.55-.36-39.72,2.4-55.68l.72-13c17.69-69.5,44.37-116.85,87.84-160.32L204,628.8l17.76-17.52,5.52-5.76c2.58-6.63,1.2-29,1.2-38.88,0-11.42-2.88-25.12-1-37l.24-29.76-.24-37.68,2.88-18.48.72-14.4,3.6-11.52.72-9.12c12.23-40.89,27.69-76.22,49.44-107.28,6.68-9.54,15.3-18.34,22.56-27.36a165.5,165.5,0,0,1,24-24c2.84-2.3,5.31-7,8.88-8.4l2.64.24,2.64-3.12,12-10.32,2.88.24,9.36-8.4,2.64.24,5.28-4.8c1.43-.62,3.22-.2,4.32-.72l2.16-3.6c5.28,0,4.6-1.83,7.2-3.6,4.19-2.85,5.28.63,7.92-5,5.87-.59,7.08-.23,9.6-4.32,2.73.07,6.45.2,8.4-.72l1.44-3.84h7.68l3.12-3.36,8.4-.48,4.08-3.84,12-.72,4.8-4.08h13.44l5-3.6H494.6l15.6-3.36,36.24-.24,71.52,1c9,0,31.14,2.41,37.92-.24,5-2,8.44-7,12-10.56l21.6-22.08,33.12-33.6c9.59-12,23-24.51,35-34.08L769.4,63.36c7.37-5.24,14.67-9.63,22.08-14.88,1.83-1.3,3.78-2.46,5.76-3.84l2.64-3.12h3.84L807.8,37h3.6l5.52-4.56h3.6l4.32-3.6,4.32-.48,4.32-4.08,5-.24,4.56-4.32,6.72-.24,4.32-3.36,7.68-1.44L864.44,12,875,10.8l3.12-3.6L889.64,7l3.6-4.08c4.54-2.38,15,1.13,20.64-.24A58.28,58.28,0,0,0,921.08.24Z" transform="translate(-53.22 -0.24)" style="fill:#fff;fill-rule:evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/desktop/badges/-1.ico
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/desktop/badges/1.ico
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
assets/desktop/badges/10.ico
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/desktop/badges/2.ico
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/desktop/badges/3.ico
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
assets/desktop/badges/4.ico
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/desktop/badges/5.ico
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
assets/desktop/badges/6.ico
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
assets/desktop/badges/7.ico
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/desktop/badges/8.ico
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
assets/desktop/badges/9.ico
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
assets/desktop/badges/badges.penpot
Normal file
BIN
assets/desktop/hicolor/128x128.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/desktop/hicolor/16x16.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
assets/desktop/hicolor/256x256.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
assets/desktop/hicolor/32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/desktop/hicolor/512x512.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/desktop/hicolor/64x64.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/desktop/icon.ico
Normal file
|
After Width: | Height: | Size: 361 KiB |
BIN
assets/desktop/icon.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
12
assets/desktop/icon.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 307 307" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-3137.299793,-1719.592766)">
|
||||
<g transform="matrix(0.902657,0,0,0.902657,2863.823104,1374.738098)">
|
||||
<path d="M356.97,675.638C323.753,644.676 302.968,600.546 302.968,551.6C302.968,458.019 378.944,382.044 472.524,382.044C566.105,382.044 642.08,458.019 642.08,551.6C642.08,629.48 589.46,695.166 517.864,715.015C501.974,691.667 487.572,670.399 498.34,635.393C485.202,642.099 476.944,655.288 474.445,669.626C469.556,661.943 464.129,654.533 457.841,647.931C441.837,631.125 420.016,619.373 409.919,597.549C405.306,587.241 402.145,572.504 405.799,561.838C408.436,582.54 418.507,601.587 436.545,612.617C450.296,621.023 465.703,622.408 481.126,625.661C497.203,629.048 522.738,636.688 538.403,633.928C551.425,631.638 578.749,617.255 578.02,601.656C577.765,596.289 571.213,587.245 568.79,582.066C565.566,575.171 567.48,573.682 567.848,566.765C568.885,547.188 563.715,537.516 554.206,521.395C542.674,501.84 530.016,490.024 508.095,482.087C488.062,474.831 457.231,478.938 456.996,479.271C461.046,483.711 464.773,488.906 465.295,495.055C457.084,487.46 449.14,479.387 439.945,472.948C419.607,458.71 406.868,460.052 398.782,484.817C394.568,497.72 391.382,515.255 395.288,528.453C397.657,536.447 402.575,542.952 408.175,548.951C397.217,547.458 390.574,537.94 388.158,527.623C360.997,586.373 354.951,614.782 356.97,675.638ZM553.254,508.59C555.938,471.688 555.624,447.806 521.4,481.304C534.318,487.312 545.181,496.897 553.254,508.59ZM486.399,552.769C503.076,550.51 505.109,576.334 489.02,577.418C472.931,578.502 471.118,554.84 486.399,552.769ZM559.47,568.921C555.834,562.599 554.722,560.76 552.607,556.182C551.168,553.064 550.439,549.366 555.655,549.256C565.117,549.055 568.395,564.041 561.026,569.27C560.844,569.397 560.628,569.465 560.407,569.465C560.02,569.465 559.662,569.257 559.47,568.921ZM430.213,501.5C431.753,505.171 434.926,507.616 437.667,510.309C431.445,512.197 425.166,510.394 419.704,507.264C420.028,513.033 424.299,518.121 427.498,522.174C422.225,523.384 417.564,521.577 413.944,517.769C413.891,523.13 415.717,527.806 418.347,532.34C408.062,537.456 403.622,526.696 403.097,518.275C402.522,509.02 406.267,485.669 412.907,478.941C420.336,471.412 431.875,480.182 438.166,485.238C443.719,489.698 449.061,495.087 454.092,500.111C447.487,504.476 437.648,504.294 430.213,501.5ZM558.455,619.379L558.452,619.376C554.235,619.505 550.344,617.264 547.164,614.697C545.37,613.176 542.74,611.209 542.372,608.931C541.643,604.396 545.254,601.153 547.406,599.808C549.251,598.658 551.495,598.017 553.613,597.527C558.442,596.411 563.455,595.377 568.319,596.323C569.774,596.606 571.22,597.077 572.417,597.951C573.614,598.821 574.544,600.138 574.705,601.612C574.824,602.705 574.525,603.796 574.164,604.833C573.322,607.217 572.16,609.476 570.71,611.548C569.274,613.613 567.556,615.508 565.485,616.934C563.414,618.361 560.969,619.304 558.455,619.379Z" style="fill:rgb(39,32,56);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.283653,0,0,0.283653,3185.723372,1793.419676)">
|
||||
<path d="M1.131,674.022C-5.294,480.363 13.948,389.958 100.38,203C108.07,235.83 129.21,266.12 164.08,270.87C146.26,251.78 130.61,231.08 123.07,205.64C110.64,163.64 120.78,107.84 134.19,66.78C159.92,-12.03 200.46,-16.3 265.18,29.01C294.44,49.5 319.72,75.19 345.85,99.36C344.19,79.79 332.33,63.26 319.44,49.13C320.19,48.07 418.3,35 482.05,58.09C551.81,83.35 592.09,120.95 628.79,183.18C659.05,234.48 675.5,265.26 672.2,327.56C671.03,349.57 664.94,354.31 675.2,376.25C682.91,392.73 703.76,421.51 704.57,438.59C706.89,488.23 619.94,534 578.5,541.29C528.65,550.07 447.39,525.76 396.23,514.98C347.15,504.63 298.12,500.22 254.36,473.47C196.96,438.37 164.91,377.76 156.52,311.88C144.89,345.82 154.95,392.72 169.63,425.52C201.76,494.97 271.2,532.37 322.13,585.85C342.14,606.86 359.41,630.44 374.97,654.89C382.92,609.26 409.2,567.29 451.01,545.95C416.741,657.349 462.573,725.027 513.139,799.329C467.207,812.064 418.82,818.869 368.857,818.869C226.818,818.869 97.524,763.871 1.131,674.022ZM625.76,142.43C600.07,105.22 565.5,74.72 524.39,55.6C633.3,-51 634.3,25 625.76,142.43ZM413.01,283.02C364.38,289.61 370.15,364.91 421.35,361.46C472.55,358.01 466.08,275.83 413.01,283.02ZM234.21,119.87C257.87,128.76 289.18,129.34 310.2,115.45C294.19,99.46 277.19,82.31 259.52,68.12C239.5,52.03 202.78,24.12 179.14,48.08C158.01,69.49 146.09,143.8 147.92,173.25C149.59,200.05 163.72,234.29 196.45,218.01C188.08,203.58 182.27,188.7 182.44,171.64C193.96,183.76 208.79,189.51 225.57,185.66C215.39,172.76 201.8,156.57 200.77,138.21C218.15,148.17 238.13,153.91 257.93,147.9C249.21,139.33 239.11,131.55 234.21,119.87ZM642.31,494.99C650.31,494.75 658.09,491.75 664.68,487.21C671.27,482.67 676.74,476.64 681.31,470.07C685.923,463.476 689.62,456.287 692.3,448.7C693.45,445.4 694.4,441.93 694.02,438.45C693.51,433.76 690.55,429.57 686.74,426.8C682.93,424.02 678.33,422.52 673.7,421.62C658.22,418.61 642.27,421.9 626.9,425.45C620.16,427.01 613.02,429.05 607.15,432.71C600.3,436.99 588.81,447.31 591.13,461.74C592.3,468.99 600.67,475.25 606.38,480.09C616.5,488.26 628.88,495.39 642.3,494.98L642.31,494.99ZM645.54,334.42C646.149,335.489 647.289,336.152 648.52,336.152C649.225,336.152 649.913,335.935 650.49,335.53C673.94,318.89 663.51,271.2 633.4,271.84C616.8,272.19 619.12,283.96 623.7,293.88C630.43,308.45 633.97,314.3 645.54,334.42Z" style="fill:white;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/desktop/icon@2x.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/desktop/icon@3x.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
assets/desktop/iconTemplate.png
Normal file
|
After Width: | Height: | Size: 480 B |
BIN
assets/web/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/web/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/web/icon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/web/masking-512x512.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
14
assets/web/monochrome.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 307 307" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-757,-1483)">
|
||||
<g transform="matrix(1.432974,0,0,1.80298,-288.929017,-864.20063)">
|
||||
<g transform="matrix(0.629919,0,0,0.500647,539.407527,1110.972982)">
|
||||
<path d="M356.969,675.637C323.753,644.675 302.968,600.546 302.968,551.6C302.968,458.019 378.944,382.044 472.524,382.044C566.105,382.044 642.08,458.019 642.08,551.6C642.08,629.48 589.46,695.166 517.864,715.015C501.974,691.667 487.572,670.399 498.34,635.393C485.202,642.099 476.944,655.288 474.445,669.626C469.556,661.943 464.129,654.533 457.841,647.931C441.837,631.125 420.016,619.373 409.919,597.549C405.306,587.241 402.145,572.504 405.799,561.838C408.436,582.54 418.507,601.587 436.545,612.617C450.296,621.023 465.703,622.408 481.126,625.661C497.203,629.048 522.738,636.688 538.403,633.928C551.425,631.638 578.749,617.255 578.02,601.656C577.765,596.289 571.213,587.245 568.79,582.066C565.566,575.171 567.48,573.682 567.848,566.765C568.885,547.188 563.715,537.516 554.206,521.395C542.674,501.84 530.016,490.024 508.095,482.087C488.062,474.831 457.231,478.938 456.996,479.271C461.046,483.711 464.773,488.906 465.295,495.055C457.084,487.46 449.14,479.387 439.945,472.948C419.607,458.71 406.868,460.052 398.782,484.817C394.568,497.72 391.382,515.255 395.288,528.453C397.657,536.447 402.575,542.952 408.175,548.951C397.217,547.458 390.574,537.94 388.158,527.623C360.997,586.373 354.95,614.781 356.969,675.637ZM558.455,619.379L558.452,619.376C554.235,619.505 550.344,617.264 547.164,614.697C545.37,613.176 542.74,611.209 542.372,608.931C541.643,604.396 545.254,601.153 547.406,599.808C549.251,598.658 551.495,598.017 553.613,597.527C558.442,596.411 563.455,595.377 568.319,596.323C569.774,596.606 571.22,597.077 572.417,597.951C573.614,598.821 574.544,600.138 574.705,601.612C574.824,602.705 574.525,603.796 574.164,604.833C573.322,607.217 572.16,609.476 570.71,611.548C569.274,613.613 567.556,615.508 565.485,616.934C563.414,618.361 560.969,619.304 558.455,619.379ZM486.399,552.769C503.076,550.51 505.109,576.334 489.02,577.418C472.931,578.502 471.118,554.84 486.399,552.769ZM430.213,501.5C431.753,505.171 434.926,507.616 437.667,510.309C431.445,512.197 425.166,510.394 419.704,507.264C420.028,513.033 424.299,518.121 427.498,522.174C422.225,523.384 417.564,521.577 413.944,517.769C413.891,523.13 415.717,527.806 418.347,532.34C408.062,537.456 403.622,526.696 403.097,518.275C402.522,509.02 406.267,485.669 412.907,478.941C420.336,471.412 431.875,480.182 438.166,485.238C443.719,489.698 449.061,495.087 454.092,500.111C447.487,504.476 437.648,504.294 430.213,501.5ZM559.47,568.921C555.834,562.599 554.722,560.76 552.607,556.182C551.168,553.064 550.439,549.366 555.655,549.256C565.117,549.055 568.395,564.041 561.026,569.27C560.844,569.397 560.628,569.465 560.407,569.465C560.02,569.465 559.662,569.257 559.47,568.921ZM553.254,508.59C555.938,471.688 555.624,447.806 521.4,481.304C534.318,487.312 545.181,496.897 553.254,508.59Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1.730725,0,0,1.375547,-3179.052619,-90.872445)">
|
||||
<circle cx="2320.479" cy="1074.483" r="61.711" style="fill:white;fill-opacity:0;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
29
assets/web/wordmark.svg
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1076 307" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-757,-1483)">
|
||||
<g transform="matrix(1.432974,0,0,1.80298,-288.929017,-864.20063)">
|
||||
<g>
|
||||
<g transform="matrix(0.629919,0,0,0.500647,539.407527,1110.972982)">
|
||||
<path d="M356.969,675.637C323.753,644.675 302.968,600.546 302.968,551.6C302.968,458.019 378.944,382.044 472.524,382.044C566.105,382.044 642.08,458.019 642.08,551.6C642.08,629.48 589.46,695.166 517.864,715.015C501.974,691.667 487.572,670.399 498.34,635.393C485.202,642.099 476.944,655.288 474.445,669.626C469.556,661.943 464.129,654.533 457.841,647.931C441.837,631.125 420.016,619.373 409.919,597.549C405.306,587.241 402.145,572.504 405.799,561.838C408.436,582.54 418.507,601.587 436.545,612.617C450.296,621.023 465.703,622.408 481.126,625.661C497.203,629.048 522.738,636.688 538.403,633.928C551.425,631.638 578.749,617.255 578.02,601.656C577.765,596.289 571.213,587.245 568.79,582.066C565.566,575.171 567.48,573.682 567.848,566.765C568.885,547.188 563.715,537.516 554.206,521.395C542.674,501.84 530.016,490.024 508.095,482.087C488.062,474.831 457.231,478.938 456.996,479.271C461.046,483.711 464.773,488.906 465.295,495.055C457.084,487.46 449.14,479.387 439.945,472.948C419.607,458.71 406.868,460.052 398.782,484.817C394.568,497.72 391.382,515.255 395.288,528.453C397.657,536.447 402.575,542.952 408.175,548.951C397.217,547.458 390.574,537.94 388.158,527.623C360.997,586.373 354.95,614.781 356.969,675.637ZM558.455,619.379L558.452,619.376C554.235,619.505 550.344,617.264 547.164,614.697C545.37,613.176 542.74,611.209 542.372,608.931C541.643,604.396 545.254,601.153 547.406,599.808C549.251,598.658 551.495,598.017 553.613,597.527C558.442,596.411 563.455,595.377 568.319,596.323C569.774,596.606 571.22,597.077 572.417,597.951C573.614,598.821 574.544,600.138 574.705,601.612C574.824,602.705 574.525,603.796 574.164,604.833C573.322,607.217 572.16,609.476 570.71,611.548C569.274,613.613 567.556,615.508 565.485,616.934C563.414,618.361 560.969,619.304 558.455,619.379ZM486.399,552.769C503.076,550.51 505.109,576.334 489.02,577.418C472.931,578.502 471.118,554.84 486.399,552.769ZM430.213,501.5C431.753,505.171 434.926,507.616 437.667,510.309C431.445,512.197 425.166,510.394 419.704,507.264C420.028,513.033 424.299,518.121 427.498,522.174C422.225,523.384 417.564,521.577 413.944,517.769C413.891,523.13 415.717,527.806 418.347,532.34C408.062,537.456 403.622,526.696 403.097,518.275C402.522,509.02 406.267,485.669 412.907,478.941C420.336,471.412 431.875,480.182 438.166,485.238C443.719,489.698 449.061,495.087 454.092,500.111C447.487,504.476 437.648,504.294 430.213,501.5ZM559.47,568.921C555.834,562.599 554.722,560.76 552.607,556.182C551.168,553.064 550.439,549.366 555.655,549.256C565.117,549.055 568.395,564.041 561.026,569.27C560.844,569.397 560.628,569.465 560.407,569.465C560.02,569.465 559.662,569.257 559.47,568.921ZM553.254,508.59C555.938,471.688 555.624,447.806 521.4,481.304C534.318,487.312 545.181,496.897 553.254,508.59Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1.730725,0,0,1.375547,-3179.052619,-90.872445)">
|
||||
<circle cx="2320.479" cy="1074.483" r="61.711" style="fill:white;fill-opacity:0;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(4.075683,0,0,3.239275,-459.164053,-1247.728679)">
|
||||
<path d="M371.565,830.572C369.399,830.572 367.307,830.355 365.29,829.922C363.274,829.488 361.399,828.838 359.665,827.972C357.932,827.105 356.432,826.005 355.165,824.672L360.765,817.422C361.699,818.555 362.799,819.505 364.065,820.272C365.332,821.038 366.657,821.613 368.04,821.997C369.424,822.38 370.765,822.572 372.065,822.572C372.832,822.572 373.499,822.505 374.065,822.372C374.632,822.238 375.065,822.03 375.365,821.747C375.665,821.463 375.815,821.088 375.815,820.622C375.815,820.055 375.615,819.572 375.215,819.172C374.815,818.772 374.165,818.413 373.265,818.097C372.365,817.78 371.165,817.488 369.665,817.222L366.315,816.572C364.882,816.305 363.532,815.93 362.265,815.447C360.999,814.963 359.874,814.33 358.89,813.547C357.907,812.763 357.14,811.805 356.59,810.672C356.04,809.538 355.765,808.172 355.765,806.572C355.765,804.305 356.357,802.397 357.54,800.847C358.724,799.297 360.324,798.13 362.34,797.347C364.357,796.563 366.599,796.172 369.065,796.172C372.332,796.172 375.24,796.688 377.79,797.722C380.34,798.755 382.449,800.105 384.115,801.772L378.565,808.922C377.232,807.522 375.59,806.38 373.64,805.497C371.69,804.613 369.815,804.172 368.015,804.172C367.449,804.172 366.932,804.238 366.465,804.372C365.999,804.505 365.64,804.705 365.39,804.972C365.14,805.238 365.015,805.572 365.015,805.972C365.015,806.905 365.482,807.588 366.415,808.022C367.349,808.455 368.632,808.855 370.265,809.222L374.215,810.122C376.049,810.522 377.64,811.022 378.99,811.622C380.34,812.222 381.465,812.938 382.365,813.772C383.265,814.605 383.94,815.563 384.39,816.647C384.84,817.73 385.065,818.972 385.065,820.372C385.065,822.638 384.474,824.53 383.29,826.047C382.107,827.563 380.499,828.697 378.465,829.447C376.432,830.197 374.132,830.572 371.565,830.572Z"/>
|
||||
<path d="M405.109,827.587C404.564,828.157 403.683,828.785 402.465,829.472C401.165,830.205 399.515,830.572 397.515,830.572C395.382,830.572 393.674,830.197 392.39,829.447C391.107,828.697 390.182,827.688 389.615,826.422C389.049,825.155 388.765,823.722 388.765,822.122L388.765,812.172L385.265,812.172L385.265,805.372L388.765,805.372L388.765,802.572L397.765,798.572L397.765,805.372L403.765,805.372L403.765,812.172L397.765,812.172L397.765,820.822C397.765,821.455 397.924,821.938 398.24,822.272C398.557,822.605 398.999,822.772 399.565,822.772C400.132,822.772 400.707,822.588 401.29,822.222C401.777,821.916 402.199,821.518 402.558,821.026C402.745,822.809 403.242,824.449 404.049,825.947C404.363,826.531 404.717,827.078 405.109,827.587Z"/>
|
||||
<g transform="matrix(1,0,0,1,-0.799373,0)">
|
||||
<path d="M418.265,830.572C415.632,830.572 413.357,830.013 411.44,828.897C409.524,827.78 408.04,826.247 406.99,824.297C405.94,822.347 405.415,820.155 405.415,817.722C405.415,815.255 405.94,813.055 406.99,811.122C408.04,809.188 409.524,807.663 411.44,806.547C413.357,805.43 415.632,804.872 418.265,804.872C421.032,804.872 423.407,805.43 425.39,806.547C427.374,807.663 428.89,809.188 429.94,811.122C430.99,813.055 431.515,815.255 431.515,817.722C431.515,820.155 430.99,822.347 429.94,824.297C428.89,826.247 427.374,827.78 425.39,828.897C423.407,830.013 421.032,830.572 418.265,830.572ZM418.465,823.372C419.232,823.372 419.907,823.197 420.49,822.847C421.074,822.497 421.532,821.913 421.865,821.097C422.199,820.28 422.365,819.155 422.365,817.722C422.365,816.288 422.199,815.163 421.865,814.347C421.532,813.53 421.074,812.947 420.49,812.597C419.907,812.247 419.232,812.072 418.465,812.072C417.699,812.072 417.024,812.247 416.44,812.597C415.857,812.947 415.399,813.53 415.065,814.347C414.732,815.163 414.565,816.288 414.565,817.722C414.565,819.155 414.732,820.28 415.065,821.097C415.399,821.913 415.857,822.497 416.44,822.847C417.024,823.197 417.699,823.372 418.465,823.372Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-1.50815,-0.391222)">
|
||||
<path d="M447.689,814.448C447.609,814.034 447.501,813.692 447.365,813.422C447.032,812.755 446.149,812.422 444.715,812.422C443.282,812.422 441.832,812.772 440.365,813.472C438.899,814.172 437.649,815.105 436.615,816.272L432.615,809.672C433.649,808.738 434.815,807.913 436.115,807.197C437.415,806.48 438.832,805.913 440.365,805.497C441.899,805.08 443.515,804.872 445.215,804.872C448.382,804.872 450.882,805.655 452.715,807.222C454.549,808.788 455.465,811.155 455.465,814.322L455.465,821.772C455.465,822.305 455.557,822.68 455.74,822.897C455.924,823.113 456.232,823.222 456.665,823.222L458.115,823.222L458.115,830.072L453.015,830.072C452.082,830.072 451.19,829.922 450.34,829.622C449.49,829.322 448.799,828.83 448.265,828.147C447.958,827.752 447.739,827.277 447.608,826.722C447.259,827.232 446.831,827.72 446.354,828.167C444.654,829.764 442.17,830.587 439.743,830.622C437.949,830.648 436.429,830.138 435.183,829.172C433.938,828.205 433.315,826.605 433.315,824.372C433.315,822.372 433.924,820.763 435.14,819.547C436.357,818.33 438.332,817.305 441.065,816.472L447.689,814.448ZM447.465,821.468L447.465,819.936L443.415,821.372C442.582,821.672 442.024,821.955 441.74,822.222C441.457,822.488 441.315,822.788 441.315,823.122C441.315,823.522 441.475,823.83 441.794,824.047C442.114,824.263 442.592,824.372 443.231,824.372C443.97,824.372 444.608,824.297 445.146,824.147C445.684,823.997 446.129,823.772 446.482,823.472C446.835,823.172 447.095,822.788 447.263,822.322C447.356,822.063 447.424,821.778 447.465,821.468Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2.465302,0)">
|
||||
<path d="M461.865,812.172L458.365,812.172L458.365,805.372L461.865,805.372L461.865,802.572L470.865,798.572L470.865,805.372L476.865,805.372L476.865,812.172L470.865,812.172L470.865,820.822C470.865,821.455 471.024,821.938 471.34,822.272C471.657,822.605 472.099,822.772 472.665,822.772C473.232,822.772 473.807,822.588 474.39,822.222C474.974,821.855 475.465,821.355 475.865,820.722L478.315,827.472C477.782,828.072 476.865,828.738 475.565,829.472C474.265,830.205 472.615,830.572 470.615,830.572C468.482,830.572 466.774,830.197 465.49,829.447C464.207,828.697 463.282,827.688 462.715,826.422C462.149,825.155 461.865,823.722 461.865,822.122L461.865,812.172Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
|
@ -1,75 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
|
@ -1,29 +0,0 @@
|
|||
(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 })
|
||||
})();
|
||||
|
|
@ -1,688 +0,0 @@
|
|||
(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();
|
||||
|
||||
})();
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
(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();
|
||||
})();
|
||||
|
|
@ -1,605 +0,0 @@
|
|||
(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();
|
||||
})();
|
||||
|
|
@ -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,40 +0,0 @@
|
|||
(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();
|
||||
})();
|
||||
|
|
@ -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,34 +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;
|
||||
|
||||
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();
|
||||
})();
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
(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 });
|
||||
})();
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
(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 });
|
||||
})();
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
(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 });
|
||||
})();
|
||||
|
|
@ -1,397 +0,0 @@
|
|||
(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 });
|
||||
})();
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
(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 });
|
||||
})();
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
(function () {
|
||||
|
||||
if (window.__AVIA_WEB_LOADED__) return;
|
||||
window.__AVIA_WEB_LOADED__ = true;
|
||||
|
||||
const LINKTREE_URL = "https://linktr.ee/GermanAvaLilac";
|
||||
const STOAT_SERVER_URL = "https://stt.gg/GvBhcejB";
|
||||
|
||||
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');
|
||||
if (panel) {
|
||||
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
panel = document.createElement('div');
|
||||
panel.id = 'avia-quickcss-panel';
|
||||
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: '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 = 'QuickCSS';
|
||||
Object.assign(header.style, {
|
||||
padding: '14px 16px',
|
||||
fontWeight: '600',
|
||||
fontSize: '14px',
|
||||
letterSpacing: '0.3px',
|
||||
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'
|
||||
});
|
||||
|
||||
const closeBtn = document.createElement('div');
|
||||
closeBtn.textContent = '✕';
|
||||
Object.assign(closeBtn.style, {
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
right: '16px',
|
||||
cursor: 'pointer',
|
||||
opacity: '0.7',
|
||||
color: '#fff'
|
||||
});
|
||||
closeBtn.onmouseenter = () => closeBtn.style.opacity = '1';
|
||||
closeBtn.onmouseleave = () => closeBtn.style.opacity = '0.7';
|
||||
closeBtn.onclick = () => panel.style.display = 'none';
|
||||
|
||||
const editorContainer = document.createElement('div');
|
||||
editorContainer.style.flex = '1';
|
||||
|
||||
panel.appendChild(header);
|
||||
panel.appendChild(closeBtn);
|
||||
panel.appendChild(editorContainer);
|
||||
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;
|
||||
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 setIcon(button, type) {
|
||||
const oldSvg = button.querySelector('svg');
|
||||
if (oldSvg) oldSvg.remove();
|
||||
|
||||
const icons = {
|
||||
monitor: "M3 4h18v12H3V4zm2 2v8h14V6H5zm3 12h8v2H8v-2z",
|
||||
upload: "M5 20h14v-2H5v2zm7-18L5.33 9h3.84v4h4.66V9h3.84L12 2z",
|
||||
refresh: "M17.65 6.35A7.95 7.95 0 0012 4V1L7 6l5 5V7a5 5 0 11-5 5H5a7 7 0 107.75-6.65z",
|
||||
code: "M8.7 16.3L4.4 12l4.3-4.3 1.4 1.4L7.2 12l2.9 2.9-1.4 1.4zm6.6 0l-1.4-1.4L16.8 12l-2.9-2.9 1.4-1.4L19.6 12l-4.3 4.3z"
|
||||
};
|
||||
|
||||
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", icons[type]);
|
||||
svg.appendChild(path);
|
||||
|
||||
button.insertBefore(svg, button.firstChild);
|
||||
}
|
||||
|
||||
function showFontLoaderPopup() {
|
||||
removeExistingPopup();
|
||||
const popup = document.createElement('div');
|
||||
popup.id = 'avia-font-loader-popup';
|
||||
Object.assign(popup.style, {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
padding: '16px',
|
||||
background: '#1e1e1e',
|
||||
color: '#fff',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
|
||||
zIndex: 999999,
|
||||
minWidth: '320px'
|
||||
});
|
||||
popup.innerHTML = `
|
||||
<div style="margin-bottom:8px;">Paste font URL (.ttf, .woff, etc.)</div>
|
||||
<input id="avia-font-url" type="text" style="width:100%; padding:6px; margin-bottom:8px; border-radius:6px; border:none; outline:none;"/>
|
||||
<div style="display:flex; justify-content:flex-end; gap:8px;">
|
||||
<button id="avia-font-apply" style="padding:6px 12px;">Apply</button>
|
||||
<button id="avia-font-cancel" style="padding:6px 12px;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
document.getElementById('avia-font-apply').onclick = () => {
|
||||
const url = document.getElementById('avia-font-url').value;
|
||||
if (!url) return;
|
||||
localStorage.setItem('avia_custom_font_url', url);
|
||||
applyFont(url);
|
||||
alert("Font Applied.");
|
||||
popup.remove();
|
||||
};
|
||||
document.getElementById('avia-font-cancel').onclick = () => popup.remove();
|
||||
}
|
||||
|
||||
function showRemoveFontPopup() {
|
||||
removeExistingPopup();
|
||||
const popup = document.createElement('div');
|
||||
popup.id = 'avia-remove-font-popup';
|
||||
Object.assign(popup.style, {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
padding: '16px',
|
||||
background: '#1e1e1e',
|
||||
color: '#fff',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
|
||||
zIndex: 999999,
|
||||
minWidth: '280px',
|
||||
textAlign: 'center'
|
||||
});
|
||||
popup.innerHTML = `
|
||||
<div style="margin-bottom:12px;">Are you sure you want to remove the custom font?</div>
|
||||
<button id="avia-font-remove" style="padding:6px 12px;">Remove Font</button>
|
||||
<button id="avia-font-cancel" style="padding:6px 12px; margin-left:6px;">Cancel</button>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
document.getElementById('avia-font-remove').onclick = () => {
|
||||
removeFont();
|
||||
popup.remove();
|
||||
};
|
||||
document.getElementById('avia-font-cancel').onclick = () => popup.remove();
|
||||
}
|
||||
|
||||
function removeExistingPopup() {
|
||||
const existing = document.getElementById('avia-font-loader-popup') || document.getElementById('avia-remove-font-popup');
|
||||
if (existing) existing.remove();
|
||||
}
|
||||
|
||||
function applyFont(url) {
|
||||
const fontName = "CustomFont" + Date.now();
|
||||
let styleTag = document.getElementById('custom-font-style');
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.id = 'custom-font-style';
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
const ext = url.split('.').pop().toLowerCase();
|
||||
const formatMap = {
|
||||
ttf: 'truetype',
|
||||
otf: 'opentype',
|
||||
woff: 'woff',
|
||||
woff2: 'woff2',
|
||||
eot: 'embedded-opentype',
|
||||
css: 'truetype'
|
||||
};
|
||||
const format = formatMap[ext] || '';
|
||||
styleTag.textContent = `
|
||||
@font-face {
|
||||
font-family: '${fontName}';
|
||||
src: url('${url}')${format ? " format('" + format + "')" : ""};
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
body, body *:not(.material-symbols-outlined) {
|
||||
font-family: '${fontName}', sans-serif !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function removeFont() {
|
||||
localStorage.removeItem('avia_custom_font_url');
|
||||
const styleTag = document.getElementById('custom-font-style');
|
||||
if (styleTag) styleTag.remove();
|
||||
alert("Reverted Font To Original Settings.");
|
||||
}
|
||||
|
||||
(function applySavedFont() {
|
||||
const savedUrl = localStorage.getItem('avia_custom_font_url');
|
||||
if (savedUrl) applyFont(savedUrl);
|
||||
})();
|
||||
|
||||
function injectButtons() {
|
||||
const appearanceBtn = Array.from(document.querySelectorAll('a')).find(a => a.textContent.trim() === 'Appearance');
|
||||
if (!appearanceBtn) return;
|
||||
|
||||
const aviaHeader = [...document.querySelectorAll('span')]
|
||||
.find(s => s.textContent.trim() === "AVIA CLIENT SETTINGS");
|
||||
if (!aviaHeader) return;
|
||||
|
||||
const aviaContainer = aviaHeader.closest('.d_flex.flex-d_column');
|
||||
if (!aviaContainer) return;
|
||||
|
||||
const targetParent = aviaContainer.querySelector('.d_flex.flex-d_column.gap_var\\(--gap-s\\)');
|
||||
if (!targetParent) return;
|
||||
|
||||
if (!document.getElementById('stoat-fake-linktree')) {
|
||||
const linktreeBtn = appearanceBtn.cloneNode(true);
|
||||
linktreeBtn.id = 'stoat-fake-linktree';
|
||||
const textNode = Array.from(linktreeBtn.querySelectorAll('div')).find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
||||
if (textNode) textNode.textContent = "(Sanctum) Ava's Linktree";
|
||||
setIcon(linktreeBtn, "monitor");
|
||||
linktreeBtn.addEventListener('click', () => window.open(LINKTREE_URL, "_blank"));
|
||||
targetParent.appendChild(linktreeBtn);
|
||||
|
||||
const stoatBtn = appearanceBtn.cloneNode(true);
|
||||
stoatBtn.id = 'stoat-fake-stoatserver';
|
||||
const stoatTextNode = Array.from(stoatBtn.querySelectorAll('div')).find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
|
||||
if (stoatTextNode) stoatTextNode.textContent = "(Sanctum) Stoat Server";
|
||||
setIcon(stoatBtn, "monitor");
|
||||
stoatBtn.addEventListener('click', () => window.open(STOAT_SERVER_URL, "_blank"));
|
||||
targetParent.appendChild(stoatBtn);
|
||||
}
|
||||
|
||||
if (!document.getElementById('stoat-fake-loadfont')) {
|
||||
const newBtn = appearanceBtn.cloneNode(true);
|
||||
newBtn.id = 'stoat-fake-loadfont';
|
||||
const textNode = Array.from(newBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
||||
if (textNode) textNode.textContent = "(Sanctum) Font Loader";
|
||||
setIcon(newBtn, "upload");
|
||||
newBtn.addEventListener('click', showFontLoaderPopup);
|
||||
targetParent.appendChild(newBtn);
|
||||
|
||||
if (!document.getElementById('stoat-fake-removefont')) {
|
||||
const removeBtn = appearanceBtn.cloneNode(true);
|
||||
removeBtn.id = 'stoat-fake-removefont';
|
||||
const removeTextNode = Array.from(removeBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
||||
if (removeTextNode) removeTextNode.textContent = "(Sanctum) Remove selected font";
|
||||
setIcon(removeBtn, "refresh");
|
||||
removeBtn.addEventListener('click', showRemoveFontPopup);
|
||||
targetParent.appendChild(removeBtn);
|
||||
}
|
||||
}
|
||||
|
||||
if (!document.getElementById('stoat-fake-quickcss')) {
|
||||
const quickCssBtn = appearanceBtn.cloneNode(true);
|
||||
quickCssBtn.id = 'stoat-fake-quickcss';
|
||||
const quickCssTextNode = Array.from(quickCssBtn.querySelectorAll('div')).find(d => d.children.length === 0);
|
||||
if (quickCssTextNode) quickCssTextNode.textContent = "(Sanctum) QuickCSS";
|
||||
setIcon(quickCssBtn, "code");
|
||||
quickCssBtn.addEventListener('click', toggleQuickCSSPanel);
|
||||
targetParent.appendChild(quickCssBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function applyQuickCSS(css) {
|
||||
let styleTag = document.getElementById('avia-quickcss-style');
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.id = 'avia-quickcss-style';
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
styleTag.textContent = css;
|
||||
}
|
||||
|
||||
(function applySavedQuickCSS() {
|
||||
const savedCSS = localStorage.getItem('avia_quickcss');
|
||||
if (savedCSS) applyQuickCSS(savedCSS);
|
||||
})();
|
||||
|
||||
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();
|
||||
|
||||
})();
|
||||
|
|
@ -1,575 +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));
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
@ -1,479 +0,0 @@
|
|||
(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();
|
||||
|
||||
})();
|
||||
|
|
@ -1,522 +0,0 @@
|
|||
(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();
|
||||
|
||||
})();
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
[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
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?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>
|
||||
103
forge.config.ts
|
|
@ -6,37 +6,56 @@ import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
|||
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
||||
import { VitePlugin } from "@electron-forge/plugin-vite";
|
||||
import { VitePluginBuildConfig } from "@electron-forge/plugin-vite/dist/Config";
|
||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
||||
import * as fs from "fs";
|
||||
|
||||
// import { globSync } from "node:fs";
|
||||
|
||||
const STRINGS = {
|
||||
author: "izzy",
|
||||
author: "MiTHRAL",
|
||||
name: "Sanctum",
|
||||
execName: "sanctum",
|
||||
description: "Open source user-first chat platform.",
|
||||
description: "Private self-hosted chat for mithraic.space.",
|
||||
};
|
||||
|
||||
const ASSET_DIR = "assets/desktop";
|
||||
const AVIA_ASSET_DIR = "avia_assets";
|
||||
|
||||
// PLATFORM env var controls which makers are active:
|
||||
// unset → local dev (all makers)
|
||||
// linux → CI Linux build (deb + zip)
|
||||
// win32 → CI Windows build (zip only, no Wine needed)
|
||||
const CI_PLATFORM = process.env.PLATFORM;
|
||||
|
||||
const makers: ForgeConfig["makers"] = [
|
||||
new MakerSquirrel({
|
||||
name: STRINGS.name,
|
||||
authors: STRINGS.author,
|
||||
iconUrl: `https://stoat.chat/app/assets/icon-DUSNE-Pb.ico`,
|
||||
setupIcon: `${AVIA_ASSET_DIR}/icon.ico`,
|
||||
description: STRINGS.description,
|
||||
exe: `${STRINGS.execName}.exe`,
|
||||
setupExe: `${STRINGS.execName}-setup.exe`,
|
||||
copyright: "Copyright (C) 2025 Revolt Platforms LTD",
|
||||
}),
|
||||
new MakerZIP({}),
|
||||
];
|
||||
|
||||
if (!process.env.PLATFORM) {
|
||||
if (CI_PLATFORM === "linux") {
|
||||
makers.push(
|
||||
new MakerDeb({
|
||||
options: {
|
||||
productName: STRINGS.name,
|
||||
productDescription: STRINGS.description,
|
||||
categories: ["Network"],
|
||||
icon: `${ASSET_DIR}/icon.png`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!CI_PLATFORM) {
|
||||
// local dev: include everything
|
||||
makers.push(
|
||||
new MakerSquirrel({
|
||||
name: STRINGS.name,
|
||||
authors: STRINGS.author,
|
||||
iconUrl: `https://mithraic.space/app/assets/icon-DUSNE-Pb.ico`,
|
||||
setupIcon: `${ASSET_DIR}/icon.ico`,
|
||||
description: STRINGS.description,
|
||||
exe: `${STRINGS.execName}.exe`,
|
||||
setupExe: `${STRINGS.execName}-setup.exe`,
|
||||
copyright: `Copyright (C) 2025 ${STRINGS.author}`,
|
||||
}),
|
||||
new MakerAppX({
|
||||
certPass: "",
|
||||
packageExecutable: `app\\${STRINGS.execName}.exe`,
|
||||
|
|
@ -89,23 +108,24 @@ if (!process.env.PLATFORM) {
|
|||
productName: STRINGS.name,
|
||||
productDescription: STRINGS.description,
|
||||
categories: ["Network"],
|
||||
icon: `${AVIA_ASSET_DIR}/icon.png`,
|
||||
icon: `${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",
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
name: STRINGS.name,
|
||||
executableName: STRINGS.execName,
|
||||
icon: `${ASSET_DIR}/icon`,
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers,
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
build: [
|
||||
{
|
||||
entry: "src/main.ts",
|
||||
config: "vite.main.config.ts",
|
||||
|
|
@ -116,34 +136,7 @@ const customVitePluginBuild: VitePluginBuildConfig[] = [
|
|||
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 = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
name: STRINGS.name,
|
||||
executableName: STRINGS.execName,
|
||||
icon: `${AVIA_ASSET_DIR}/icon`,
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers,
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
build: customVitePluginBuild,
|
||||
],
|
||||
renderer: [],
|
||||
}),
|
||||
new FusesPlugin({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"name": "sanctum",
|
||||
"productName": "Sanctum",
|
||||
"version": "1.0.7",
|
||||
"aviaVersion": "1.0.7",
|
||||
"productName": "sanctum",
|
||||
"version": "1.3.0",
|
||||
"main": ".vite/build/main.js",
|
||||
"repository": "https://git.mithraic.cloud/ad3laid3/sanctum",
|
||||
"scripts": {
|
||||
|
|
@ -38,7 +37,6 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"electron": "38.1.2",
|
||||
"electron-vite": "^5.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"json-schema-typed": "^8.0.1",
|
||||
|
|
@ -56,5 +54,5 @@
|
|||
"electron-store": "^10.1.0",
|
||||
"utf-8-validate": "^6.0.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
}
|
||||
575
pnpm-lock.yaml
generated
BIN
screenshot.png
|
Before Width: | Height: | Size: 467 KiB After Width: | Height: | Size: 702 KiB |
67
src/config.d.ts
vendored
|
|
@ -1,15 +1,10 @@
|
|||
declare type DesktopConfig = {
|
||||
firstLaunch: boolean;
|
||||
customFrame: boolean;
|
||||
customFrameNativeMenu: boolean;
|
||||
minimiseToTray: boolean;
|
||||
disableTrayClick: boolean;
|
||||
spellchecker: boolean;
|
||||
hardwareAcceleration: boolean;
|
||||
discordRpc: boolean;
|
||||
gamePresenceEnabled: boolean;
|
||||
gamePresenceRestrictToAllowList: boolean;
|
||||
gamePresenceAllowList: string;
|
||||
windowState: {
|
||||
x: number;
|
||||
y: number;
|
||||
|
|
@ -18,65 +13,3 @@ declare type DesktopConfig = {
|
|||
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
|
|
@ -1,4 +0,0 @@
|
|||
declare module "*?asset" {
|
||||
export const assetURL: string;
|
||||
export default assetURL;
|
||||
}
|
||||
126
src/main.ts
|
|
@ -1,119 +1,41 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { BrowserWindow, app, shell } from "electron";
|
||||
import started from "electron-squirrel-startup";
|
||||
|
||||
import { aviaVersion } from "../package.json";
|
||||
|
||||
import { autoLaunch } from "./native/autoLaunch";
|
||||
import { setBadgeCount } from "./native/badges";
|
||||
import { config } from "./native/config";
|
||||
import { initDiscordRpc } from "./native/discordRpc";
|
||||
import { startGamePresenceMonitor } from "./native/gamePresence";
|
||||
import { checkForUpdates } from "./native/updater";
|
||||
import { initTray } from "./native/tray";
|
||||
import { checkForUpdates } from "./native/updater";
|
||||
import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
|
||||
|
||||
// Linux GPU sandbox causes fatal crash on some AMD/CachyOS setups
|
||||
if (process.platform === "linux") {
|
||||
app.commandLine.appendSwitch("disable-gpu-sandbox");
|
||||
app.commandLine.appendSwitch("no-zygote");
|
||||
app.commandLine.appendSwitch("use-gl", "desktop");
|
||||
app.commandLine.appendSwitch("disable-software-rasterizer");
|
||||
}
|
||||
|
||||
const applyAppName = () => {
|
||||
try {
|
||||
app.setName("Sanctum");
|
||||
app.name = "Sanctum";
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId("cloud.mithraic.sanctum");
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
// Squirrel-specific logic
|
||||
// create/remove shortcuts on Windows when installing / uninstalling
|
||||
// we just need to close out of the app immediately
|
||||
if (started) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// disable hw-accel if so requested
|
||||
if (!config.hardwareAcceleration) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// ensure only one copy of the application can run
|
||||
const acquiredLock = app.requestSingleInstanceLock();
|
||||
|
||||
const loadInject = () => {
|
||||
if (!mainWindow) return;
|
||||
|
||||
const wc = mainWindow.webContents;
|
||||
wc.removeAllListeners("dom-ready");
|
||||
wc.once("dom-ready", async () => {
|
||||
try {
|
||||
if (mainWindow.isDestroyed() || wc.isDestroyed()) return;
|
||||
|
||||
const builtInLocalPlugins = [
|
||||
{
|
||||
id: "sanctum-vcsounds",
|
||||
name: "VCSounds",
|
||||
code: fs.readFileSync(path.join(__dirname, "VCSounds.js"), "utf8"),
|
||||
enabled: true,
|
||||
locked: true,
|
||||
},
|
||||
];
|
||||
|
||||
await wc.executeJavaScript(
|
||||
`window.__SANCTUM_BUILTIN_LOCAL_PLUGINS__ = ${JSON.stringify(
|
||||
builtInLocalPlugins,
|
||||
)};`,
|
||||
true,
|
||||
);
|
||||
|
||||
if (mainWindow.isDestroyed() || wc.isDestroyed()) return;
|
||||
|
||||
const plugins: string[] = [
|
||||
"inject.js",
|
||||
"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",
|
||||
];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (mainWindow.isDestroyed() || wc.isDestroyed()) return;
|
||||
const pluginPath: string = path.join(__dirname, plugin);
|
||||
const pluginCode: string = fs.readFileSync(pluginPath, "utf8");
|
||||
await wc.executeJavaScript(pluginCode, true);
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (acquiredLock) {
|
||||
app.whenReady().then(() => {
|
||||
applyAppName();
|
||||
// create and configure the app when electron is ready
|
||||
app.on("ready", () => {
|
||||
// create window and application contexts
|
||||
createMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.setTitle("Sanctum");
|
||||
mainWindow.on("page-title-updated", (e) => {
|
||||
e.preventDefault();
|
||||
mainWindow.setTitle("Sanctum");
|
||||
});
|
||||
}
|
||||
loadInject();
|
||||
|
||||
// enable auto start on Windows and MacOS
|
||||
if (config.firstLaunch) {
|
||||
if (process.platform === "win32" || process.platform === "darwin") {
|
||||
autoLaunch.enable();
|
||||
|
|
@ -123,27 +45,24 @@ if (acquiredLock) {
|
|||
|
||||
initTray();
|
||||
initDiscordRpc();
|
||||
startGamePresenceMonitor();
|
||||
checkForUpdates();
|
||||
setBadgeCount(0);
|
||||
|
||||
// Windows specific fix for notifications
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId("cloud.mithraic.sanctum");
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
app.setAboutPanelOptions({
|
||||
version: aviaVersion,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// focus the window if we try to launch again
|
||||
app.on("second-instance", () => {
|
||||
mainWindow.show();
|
||||
mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
// macOS specific behaviour to keep app active in dock:
|
||||
// (irrespective of the minimise-to-tray option)
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
|
|
@ -153,27 +72,22 @@ if (acquiredLock) {
|
|||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.setTitle("Sanctum");
|
||||
mainWindow.on("page-title-updated", (e) => {
|
||||
e.preventDefault();
|
||||
mainWindow.setTitle("Sanctum");
|
||||
});
|
||||
}
|
||||
loadInject();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// ensure URLs launch in external context
|
||||
app.on("web-contents-created", (_, contents) => {
|
||||
// prevent navigation out of build URL origin
|
||||
contents.on("will-navigate", (event, navigationUrl) => {
|
||||
if (new URL(navigationUrl).origin !== BUILD_URL.origin) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// handle links externally
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (
|
||||
url.startsWith("http:") ||
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
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,10 @@ import AutoLaunch from "auto-launch";
|
|||
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { mainWindow } from "./window";
|
||||
|
||||
export const autoLaunch = new AutoLaunch({
|
||||
name: "Stoat",
|
||||
name: "Sanctum",
|
||||
});
|
||||
|
||||
ipcMain.handle("getAutostart", async () => {
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ export async function setBadgeCount(count: number) {
|
|||
signature: "sa{sv}",
|
||||
body: [
|
||||
process.env.container === "1"
|
||||
? "application://chat.stoat.stoat-desktop.desktop" // flatpak handling
|
||||
: "application://stoat-desktop.desktop",
|
||||
? "application://cloud.mithraic.sanctum.desktop" // flatpak handling
|
||||
: "application://sanctum.desktop",
|
||||
[
|
||||
["count", ["x", Math.min(count, 0)]],
|
||||
["count-visible", ["b", count !== 0]],
|
||||
|
|
|
|||
|
|
@ -13,15 +13,9 @@ const schema = {
|
|||
customFrame: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
customFrameNativeMenu: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
minimiseToTray: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
disableTrayClick: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
startMinimisedToTray: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
|
|
@ -34,15 +28,6 @@ const schema = {
|
|||
discordRpc: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
gamePresenceEnabled: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
gamePresenceRestrictToAllowList: {
|
||||
type: "boolean",
|
||||
} as JSONSchema.Boolean,
|
||||
gamePresenceAllowList: {
|
||||
type: "string",
|
||||
} as JSONSchema.String,
|
||||
windowState: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
@ -70,16 +55,11 @@ const store = new Store({
|
|||
defaults: {
|
||||
firstLaunch: true,
|
||||
customFrame: true,
|
||||
customFrameNativeMenu: false,
|
||||
minimiseToTray: true,
|
||||
disableTrayClick: false,
|
||||
startMinimisedToTray: false,
|
||||
spellchecker: true,
|
||||
hardwareAcceleration: true,
|
||||
discordRpc: true,
|
||||
gamePresenceEnabled: true,
|
||||
gamePresenceRestrictToAllowList: true,
|
||||
gamePresenceAllowList: "",
|
||||
windowState: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
|
@ -98,16 +78,11 @@ class Config {
|
|||
mainWindow.webContents.send("config", {
|
||||
firstLaunch: this.firstLaunch,
|
||||
customFrame: this.customFrame,
|
||||
customFrameNativeMenu: this.customFrameNativeMenu,
|
||||
minimiseToTray: this.minimiseToTray,
|
||||
disableTrayClick: this.disableTrayClick,
|
||||
startMinimisedToTray: this.startMinimisedToTray,
|
||||
spellchecker: this.spellchecker,
|
||||
hardwareAcceleration: this.hardwareAcceleration,
|
||||
discordRpc: this.discordRpc,
|
||||
gamePresenceEnabled: this.gamePresenceEnabled,
|
||||
gamePresenceRestrictToAllowList: this.gamePresenceRestrictToAllowList,
|
||||
gamePresenceAllowList: this.gamePresenceAllowList,
|
||||
windowState: this.windowState,
|
||||
});
|
||||
}
|
||||
|
|
@ -138,34 +113,6 @@ class Config {
|
|||
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() {
|
||||
return (store as never as { get(k: string): boolean }).get(
|
||||
"minimiseToTray",
|
||||
|
|
@ -245,47 +192,6 @@ class Config {
|
|||
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() {
|
||||
return (
|
||||
store as never as { get(k: string): DesktopConfig["windowState"] }
|
||||
|
|
|
|||
|
|
@ -3,32 +3,7 @@ import { Client } from "discord-rpc";
|
|||
import { config } from "./config";
|
||||
|
||||
// internal state
|
||||
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 */
|
||||
}
|
||||
}
|
||||
let rpc: Client;
|
||||
|
||||
export async function initDiscordRpc() {
|
||||
if (!config.discordRpc) return;
|
||||
|
|
@ -39,24 +14,31 @@ export async function initDiscordRpc() {
|
|||
try {
|
||||
rpc = new Client({ transport: "ipc" });
|
||||
|
||||
rpc.on("ready", applyActivity);
|
||||
rpc.on("ready", () =>
|
||||
rpc.setActivity({
|
||||
state: "mithraic.space",
|
||||
details: "Chatting with others",
|
||||
largeImageKey: "qr",
|
||||
largeImageText: "Join Sanctum!",
|
||||
buttons: [
|
||||
{
|
||||
label: "Join Sanctum",
|
||||
url: "https://mithraic.space/",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
rpc.on("disconnected", reconnect);
|
||||
|
||||
rpc.login({ clientId: "1490783938829090837" });
|
||||
rpc.login({ clientId: "872068124005007420" });
|
||||
} catch (err) {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export function setDiscordActivity(activity: RpcActivity | null) {
|
||||
pendingActivity = activity ?? defaultActivity;
|
||||
applyActivity();
|
||||
}
|
||||
|
||||
const reconnect = () => setTimeout(() => initDiscordRpc(), 1e4);
|
||||
|
||||
export async function destroyDiscordRpc() {
|
||||
rpc?.destroy();
|
||||
rpc = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
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)));
|
||||
}
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,335 +0,0 @@
|
|||
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,12 +1,11 @@
|
|||
import { Menu, Tray, app, nativeImage } from "electron";
|
||||
import { Menu, Tray, nativeImage } from "electron";
|
||||
|
||||
import trayIconAsset from "../../avia_assets/icon.png?asset";
|
||||
import macOsTrayIconAsset from "../../avia_assets/iconTemplate.png?asset";
|
||||
import { aviaVersion, version } from "../../package.json";
|
||||
import trayIconAsset from "../../assets/desktop/icon.png?asset";
|
||||
import macOsTrayIconAsset from "../../assets/desktop/iconTemplate.png?asset";
|
||||
import { version } from "../../package.json";
|
||||
|
||||
import { createAboutWindow } from "./about";
|
||||
import { config } from "./config";
|
||||
import { mainWindow, quitApp } from "./window";
|
||||
import { checkForUpdates } from "./updater";
|
||||
|
||||
// internal tray state
|
||||
let tray: Tray = null;
|
||||
|
|
@ -27,13 +26,9 @@ export function initTray() {
|
|||
const trayIcon = createTrayIcon();
|
||||
tray = new Tray(trayIcon);
|
||||
updateTrayMenu();
|
||||
tray.setToolTip("Sanctum for Desktop");
|
||||
tray.setToolTip(`Sanctum ${version}`);
|
||||
tray.setImage(trayIcon);
|
||||
tray.on("click", () => {
|
||||
config.sync();
|
||||
if (config.disableTrayClick) {
|
||||
return;
|
||||
}
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
|
|
@ -46,29 +41,22 @@ export function initTray() {
|
|||
export function updateTrayMenu() {
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{ label: "Sanctum for Desktop", type: "normal", enabled: false },
|
||||
{ label: "Sanctum", type: "normal", enabled: false },
|
||||
{
|
||||
label: "Versions",
|
||||
label: "Version",
|
||||
type: "submenu",
|
||||
submenu: Menu.buildFromTemplate([
|
||||
{
|
||||
label: `Stoat Desktop: ${version}`,
|
||||
type: "normal",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: `Sanctum: ${aviaVersion}`,
|
||||
label: version,
|
||||
type: "normal",
|
||||
enabled: false,
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
label: "About",
|
||||
label: "Check for Updates",
|
||||
type: "normal",
|
||||
click() {
|
||||
createAboutWindow();
|
||||
},
|
||||
click: () => checkForUpdates(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
|
|
@ -82,15 +70,6 @@ export function updateTrayMenu() {
|
|||
}
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Restart App",
|
||||
type: "normal",
|
||||
click() {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Quit App",
|
||||
type: "normal",
|
||||
|
|
|
|||
|
|
@ -27,22 +27,17 @@ ipcRenderer.on('upd-progress',(_,p)=>{
|
|||
document.getElementById('status').textContent=p<100?'Downloading… '+p+'%':'Installing…';
|
||||
});
|
||||
ipcRenderer.on('upd-ready',(_,v)=>{
|
||||
document.getElementById('title').textContent='Sanctum '+v+' installed';
|
||||
document.getElementById('title').textContent='Sanctum '+v+' Ready';
|
||||
document.getElementById('status').textContent='Restart to apply the update.';
|
||||
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);
|
||||
document.getElementById('btn').style.display='block';
|
||||
});
|
||||
ipcRenderer.on('upd-error',(_,msg)=>{
|
||||
document.getElementById('title').textContent='Update Failed';
|
||||
document.getElementById('status').textContent=msg;
|
||||
document.getElementById('bar').style.background='#f38ba8';
|
||||
});
|
||||
document.getElementById('btn').onclick=()=>ipcRenderer.send('upd-restart');
|
||||
</script></body></html>`;
|
||||
|
||||
export function showUpdateWindow() {
|
||||
|
|
@ -70,14 +65,15 @@ export function setUpdateProgress(percent: number) {
|
|||
win?.webContents.send("upd-progress", Math.round(percent));
|
||||
}
|
||||
|
||||
export function setUpdateReady(version: string, relaunch: boolean) {
|
||||
export function setUpdateReady(version: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
ipcMain.on("upd-restart", () => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Notification, app, ipcMain } from "electron";
|
||||
import { exec, spawn } from "child_process";
|
||||
import { createWriteStream, mkdirSync, writeFileSync } from "fs";
|
||||
import { exec } from "child_process";
|
||||
import { createWriteStream, mkdirSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
|
||||
|
|
@ -96,46 +96,19 @@ async function downloadAndInstall(url: string, version: string) {
|
|||
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) => {
|
||||
const cmd =
|
||||
process.platform === "win32"
|
||||
? `powershell -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'; $sub = (Get-ChildItem '${extractDir}' | Select-Object -First 1).FullName; Copy-Item -Recurse -Force \\"$sub\\*\\" '${installDir}'"`
|
||||
: `unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && rm -f "${installDir}/sanctum" && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`;
|
||||
|
||||
exec(cmd, { shell: process.platform === "win32" ? undefined : "/bin/bash" }, (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);
|
||||
}
|
||||
setUpdateReady(version);
|
||||
}
|
||||
|
||||
function notify(title: string, body: string) {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import {
|
|||
nativeImage,
|
||||
} from "electron";
|
||||
|
||||
import windowIconAsset from "../../avia_assets/icon.png?asset";
|
||||
import windowIconAsset from "../../assets/desktop/icon.png?asset";
|
||||
|
||||
import { aboutWindow, createAboutWindow } from "./about";
|
||||
import { config } from "./config";
|
||||
import { updateTrayMenu } from "./tray";
|
||||
|
||||
|
|
@ -49,18 +48,6 @@ export function createMainWindow() {
|
|||
height: 720,
|
||||
backgroundColor: "#191919",
|
||||
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,
|
||||
show: !startHidden,
|
||||
webPreferences: {
|
||||
|
|
@ -69,7 +56,6 @@ export function createMainWindow() {
|
|||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
spellcheck: true,
|
||||
devTools: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -146,59 +132,19 @@ export function createMainWindow() {
|
|||
// reset zoom to default.
|
||||
event.preventDefault();
|
||||
mainWindow.webContents.setZoomLevel(0);
|
||||
} else if (input.key === "F1") {
|
||||
event.preventDefault();
|
||||
createAboutWindow();
|
||||
} else if (
|
||||
input.key === "F5" ||
|
||||
((input.control || input.meta) && input.key.toLowerCase() === "r")
|
||||
) {
|
||||
event.preventDefault();
|
||||
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();
|
||||
})();`);
|
||||
}
|
||||
});
|
||||
|
||||
const initialCustomFrame: boolean = config.customFrame;
|
||||
const initialCFNM: boolean = config.customFrameNativeMenu;
|
||||
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
// send the config
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
injectBranding(mainWindow.webContents);
|
||||
});
|
||||
|
||||
// configure spellchecker context menu
|
||||
|
|
@ -257,6 +203,76 @@ export function createMainWindow() {
|
|||
// setInterval(() => setBadgeCount((++i % 30) + 1), 1000);
|
||||
}
|
||||
|
||||
function injectBranding(wc: Electron.WebContents) {
|
||||
const logoUrl = windowIconAsset;
|
||||
wc.insertCSS(`
|
||||
[class*="wordmark"], [class*="Wordmark"], [data-app-name] { display: none !important; }
|
||||
`);
|
||||
wc.executeJavaScript(`
|
||||
(function() {
|
||||
const LOGO = ${JSON.stringify(logoUrl)};
|
||||
const BRAND_RE = /\\b(Revolt|Stoat)\\b/g;
|
||||
const SKIP_TAGS = new Set(['SCRIPT','STYLE','TEXTAREA','INPUT','CODE','PRE']);
|
||||
|
||||
function patchImages() {
|
||||
document.querySelectorAll('img').forEach(function(img) {
|
||||
var src = img.getAttribute('src') || '';
|
||||
var alt = (img.getAttribute('alt') || '').toLowerCase();
|
||||
if (
|
||||
src.includes('revolt') || src.includes('stoat') ||
|
||||
alt === 'revolt' || alt === 'stoat' ||
|
||||
(src.startsWith('/') && /\\.(svg|png|webp)/.test(src) && /revolt|stoat|logo/i.test(alt))
|
||||
) {
|
||||
img.src = LOGO;
|
||||
img.removeAttribute('srcset');
|
||||
img.alt = 'Sanctum';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function patchText(root) {
|
||||
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
var node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (SKIP_TAGS.has(node.parentElement && node.parentElement.tagName)) continue;
|
||||
if (BRAND_RE.test(node.nodeValue)) {
|
||||
node.nodeValue = node.nodeValue.replace(BRAND_RE, 'Sanctum');
|
||||
}
|
||||
BRAND_RE.lastIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function patchTitle() {
|
||||
if (document.title && BRAND_RE.test(document.title)) {
|
||||
document.title = document.title.replace(BRAND_RE, 'Sanctum');
|
||||
}
|
||||
BRAND_RE.lastIndex = 0;
|
||||
}
|
||||
|
||||
function patch(root) {
|
||||
patchImages();
|
||||
patchText(root || document.body);
|
||||
patchTitle();
|
||||
}
|
||||
|
||||
patch(document.documentElement);
|
||||
|
||||
new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(m) {
|
||||
m.addedNodes.forEach(function(n) {
|
||||
if (n.nodeType === 1) patch(n);
|
||||
else if (n.nodeType === 3 && !SKIP_TAGS.has(n.parentElement && n.parentElement.tagName)) {
|
||||
if (BRAND_RE.test(n.nodeValue)) n.nodeValue = n.nodeValue.replace(BRAND_RE, 'Sanctum');
|
||||
BRAND_RE.lastIndex = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
patchTitle();
|
||||
}).observe(document.documentElement, { childList: true, subtree: true });
|
||||
})();
|
||||
`, true).catch(function() {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit the entire app
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
import { aviaVersion, version } from "../../package.json";
|
||||
import { version } from "../../package.json";
|
||||
|
||||
contextBridge.exposeInMainWorld("native", {
|
||||
versions: {
|
||||
|
|
@ -8,23 +8,6 @@ contextBridge.exposeInMainWorld("native", {
|
|||
chrome: () => process.versions.chrome,
|
||||
electron: () => process.versions.electron,
|
||||
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"),
|
||||
|
|
|
|||