Compare commits

...

173 commits

Author SHA1 Message Date
MiTHRAL
4f5cbbb3c2 chore: rebrand UI to Sanctum, bump to v1.0.0
Some checks are pending
/ Build App (push) Waiting to run
Replace all user-visible "Avia"/"AviaClient" strings with "Sanctum" equivalents
across all UI components. Update version to 1.0.0 in package.json and add release
notes to metainfo.xml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 17:42:21 -04:00
MiTHRAL
cc8ba75694 chore: replace update-electron-app with Gitea-backed updater 2026-04-22 17:30:21 -04:00
MiTHRAL
225c623ecb chore: point to self-hosted instance at mithraic.space 2026-04-22 17:27:58 -04:00
MiTHRAL
1eb09a589a chore: rebrand to Sanctum for self-hosted instance 2026-04-22 17:25:22 -04:00
AvaLilac
821ff30d40
Add files via upload
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-07 09:55:15 -04:00
AvaLilac
d98d6d5441
Delete avia_assets/icon.png
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-07 09:54:30 -04:00
AvaLilac
ac7f85f679
Merge pull request #2 from AvaLilac/dev
Dev
2026-04-06 22:35:03 -04:00
AvaLilac
222d796843
Merge branch 'stoatchat:main' into dev 2026-04-06 22:27:08 -04:00
Amelia
0abe72a3c6
Add Aurora to the README
Signed-off-by: Amelia <afrosty.skye@gmail.com>
2026-04-06 18:48:13 -07:00
Amelia Frost
4b05dd4fbe
Our kitty is named Aurora. 2026-04-06 16:59:03 -07:00
Amelia Frost
0e40c6c6ec
Remove 16x16 and 24x24 2026-04-06 16:44:21 -07:00
Amelia Frost
4f13d893cd
New Icon 2026-04-06 16:33:57 -07:00
AvaLilac
593271c2b9
Merge MonacoCSS Plugin directly into QuickCSS as the native panel
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-06 18:44:10 -04:00
AvaLilac
dec110d795
Merge MonacoCSS Plugin directly into Themes as the native panel
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-06 18:43:09 -04:00
AvaLilac
8b0e6588fd
Change Discord RPC to aviaclients
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-06 14:52:47 -04:00
Amelia Frost
0c2c081ab0
Focus on the main window when the about window is closed 2026-04-06 00:14:31 -07:00
Amelia Frost
35ee57483a
Allow closing the about window by pressing Escape 2026-04-05 23:15:07 -07:00
Amelia Frost
a632ecde33
Don't show more than a single about window 2026-04-05 23:14:39 -07:00
Amelia Frost
4f845f58a9
Add our about window to the tray icon 2026-04-05 21:55:23 -07:00
Amelia Frost
d165a77875
Add the stoat version we're based on 2026-04-05 21:47:52 -07:00
Amelia Frost
7ca75e859e
Lower the width of the about window 2026-04-05 21:14:43 -07:00
Amelia Frost
c330333ee6
Replace version with the version from package.json 2026-04-05 21:14:08 -07:00
Amelia Frost
c15d64164b
Format about.html 2026-04-05 20:26:13 -07:00
Amelia Frost
4208ee0ec2
do not allow minimizing, and set the window handle to null on close 2026-04-05 20:17:06 -07:00
Amelia Frost
50b05be8fc
Get rid of the menu on our about window 2026-04-05 20:16:25 -07:00
AvaLilac
a81c7b092b
get the window to display
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-05 22:59:17 -04:00
Amelia Frost
5d1a5e3ecb
Partial about window implementation 2026-04-05 19:36:13 -07:00
AvaLilac
b3c4959c7c
Add a about page inside avia client. step 1
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-05 20:48:39 -04:00
Amelia Frost
841a9be2cf
Reapply "Prevent the page keydown/keyup events and menu shortcuts"
This reverts commit 60fb61a1db.
2026-04-05 16:59:30 -07:00
Amelia Frost
60fb61a1db
Revert "Prevent the page keydown/keyup events and menu shortcuts"
This reverts commit 24c9cc51f4.
2026-04-05 15:26:04 -07:00
Amelia Frost
24c9cc51f4
Prevent the page keydown/keyup events and menu shortcuts 2026-04-05 14:27:48 -07:00
Amelia Frost
da383611c0
on macOS, open/close Settings with Cmd+, 2026-04-04 20:54:16 -07:00
Amelia Frost
741102d6eb
Show AviaClient's version in the about window 2026-04-04 16:23:39 -07:00
Amelia Frost
22785a9860
Use our exposed versions 2026-04-04 15:43:57 -07:00
Amelia Frost
e56455fbaf
Show AviaClient version is tray, and expose it to world 2026-04-04 15:26:59 -07:00
Amelia Frost
0c2d23d029
Make it a toggle 2026-04-04 12:35:06 -07:00
AvaLilac
363f9f675b
Make it possible for users to open devtools via F12
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-04 15:27:29 -04:00
AvaLilac
c386cb6cba
Update README.md
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-04 15:00:59 -04:00
AvaLilac
b5f8edeb4d
Update README.md
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-04 14:58:54 -04:00
Amelia Frost
b9a3d37c9a
partial fix for badges being displayed 2026-04-02 16:52:56 -07:00
AvaLilac
962e4b188b
Update README.md
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-02 12:57:03 -04:00
Amelia Frost
6fbd8a18f6
Load Token Login internal plugin 2026-04-01 15:19:23 -07:00
AvaLilac
b22130645a
adds the ability to login to stoat desktop with your session token
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-04-01 18:08:46 -04:00
Amelia Frost
7d07e3fcc8
refactor: prefer using app.whenReady() to avoid a race condition 2026-04-01 00:40:26 -07:00
Amelia Frost
b59ab85e7a
Update pnpm to 10.33.0 2026-03-31 23:47:36 -07:00
Amelia Frost
5a3c1458ac
New package lock 2026-03-31 01:24:55 -07:00
Amelia Frost
262c74fefe
Update update-electron-app 2026-03-31 01:24:39 -07:00
Amelia Frost
da075829dc
Install electron-vite to fix types warning 2026-03-31 01:24:10 -07:00
Amelia Frost
60dbbde2af
fix formatting and linter warnings 2026-03-31 01:22:15 -07:00
AvaLilac
c0eb4129ac
added full codeberg support
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-30 19:46:15 -04:00
Amelia Frost
2c7a864815
ESLint should ignore assets 2026-03-29 17:36:31 -07:00
Amelia Frost
590729ccff
Remove unused import 2026-03-29 17:34:57 -07:00
Amelia Frost
c407a89142
only include TypeScript & JavaScript in compilation 2026-03-29 17:18:06 -07:00
AvaLilac
fa6cfb86f3
Update main.ts
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-29 17:58:17 -04:00
AvaLilac
b292308cd5
Allows you to completely backup avia client and restore it
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-29 23:56:59 +02:00
Amelia Frost
a60a60ab4b
very minor refactor 2026-03-29 00:45:05 -07:00
Amelia Frost
2f2b4474b4
Add option to disable showing/hiding on clicking the tray icon 2026-03-28 19:55:40 -07:00
Amelia Frost
0e1c696a68
Replace macOS placeholder icon with an .icon from Icon Composer 2026-03-28 18:19:25 -07:00
Amelia Frost
f31c2ca067
Revert "Changed tray icon behaviour to not show/hide on click (as we have a Show App/Hide App sub-menu)"
This reverts commit 85aaf5946d.
2026-03-28 18:16:29 -07:00
Amelia Frost
fbf53123fa
Add a seperator before Restart/Quit 2026-03-28 18:13:17 -07:00
Amelia Frost
85aaf5946d
Changed tray icon behaviour to not show/hide on click (as we have a Show App/Hide App sub-menu) 2026-03-28 18:12:35 -07:00
Amelia Frost
cec88cfe3f
fix: compile 2026-03-28 18:10:14 -07:00
AvaLilac
0e903e71df
Added the Ability to restart the entire client
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 21:06:46 -04:00
Amelia Frost
f95cc126ce
fix: missing semi-colons 2026-03-28 17:33:34 -07:00
Amelia Frost
09662fc37e
Add the system's native menu to the titlebar, configurable 2026-03-28 17:22:40 -07:00
AvaLilac
efbba2a65f
change 1.5.0 to 1.6.0
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 19:00:19 -04:00
AvaLilac
149930460b
Update main.tsadded aviaclientdesktop
and fixed a issue that made it not compile

Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 18:46:20 -04:00
AvaLilac
05decfbae4
changes the stoat desktop button in desktop settings to make it avia
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 18:45:02 -04:00
AvaLilac
727ba0f1df
intergrated headliner
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 18:23:54 -04:00
AvaLilac
a7115dc1d4
we want the community to be involved in making builds
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 17:12:19 -04:00
AvaLilac
5e0b70056e
allows you to change stoats text
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 17:06:04 -04:00
Amelia Frost
a63ce3c41f
Fix missing comma 2026-03-28 13:33:38 -07:00
AvaLilac
0dd42efcaa
Update main.ts
Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 16:19:39 -04:00
AvaLilac
ac1b93677c
Fix's toolbar button injection
This Plugin is integrated as it fix's the bug where the buttons inside the tooltip do not hide when you look in a channel where you cant talk in. making the buttons push onto the other side till you reload the chat bar. futureproof. 

This plugin is made by 0simp#2291
Full credits to them for making this possible

Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
2026-03-28 16:19:08 -04:00
AvaLilac
73af6e3062
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-28 15:05:51 -04:00
AvaLilac
47e524f0d2
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-28 11:31:07 -04:00
AvaLilac
191e23b61f
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-27 09:42:13 -04:00
Amelia Frost
94610845be
macOS placeholder icon 2026-03-26 15:41:26 -07:00
AvaLilac
34fd294be9
Added Drag and drop and a export feature
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-26 17:56:26 -04:00
Amelia Frost
7110c2202f
Automate building of avia_core files 2026-03-25 16:37:26 -07:00
AvaLilac
e5731054a7
Update main.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-25 18:18:24 -04:00
AvaLilac
1e8310874c
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-25 18:18:00 -04:00
AvaLilac
95715a4fcd
Add files via upload
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-25 18:17:22 -04:00
AvaLilac
a61fbb695a
Add Import button
This commit adds the ability to import plugins without having to create a plugin and copy and paste code

you can

Drag And Drop into the panel to add plugins
Click Import and either import 1 .js plugin or as many as you want

Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-25 13:17:00 -04:00
AvaLilac
b2bb58b9f0
Add the exe icon
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:40:48 -04:00
Amelia Frost
97662d2e15
Use new icon path 2026-03-24 16:35:38 -07:00
AvaLilac
a9f67aa099
Delete src/themes.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:54 -04:00
AvaLilac
0b849eb62b
Delete src/pluginsupport.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:47 -04:00
AvaLilac
ff169347df
Delete src/inject.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:40 -04:00
AvaLilac
b6cd8524dc
Delete src/aviaversion.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:32 -04:00
AvaLilac
a8af809134
Delete src/aviafavsystem.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:20 -04:00
AvaLilac
ef57607bc6
Delete src/aviaclientcategory.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:12 -04:00
AvaLilac
63d735f265
Delete src/LocalPlugins.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:16:04 -04:00
AvaLilac
5a0959d721
Have It compile from the new folder
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:14:03 -04:00
AvaLilac
82c81a6424
Seperating All of avia client's stuff out of SRC and into its own folder
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 19:12:47 -04:00
Amelia Frost
4bcbd24d99
Merge branch 'main' into dev 2026-03-24 16:11:50 -07:00
Amelia Frost
671e67cde4
Fix file indention 2026-03-24 16:05:21 -07:00
AvaLilac
0e8441e709
Change icon to Placeholder
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 18:44:10 -04:00
AvaLilac
8d32b73d98
Delete avia_assets/icon.png
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 18:43:39 -04:00
AvaLilac
02003514df
Add files via upload
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 18:43:11 -04:00
AvaLilac
a1463ea803
Delete avia_assets/icon.png
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 18:42:43 -04:00
AvaLilac
457f2e5b21
Merge branch 'stoatchat:main' into dev 2026-03-24 18:36:23 -04:00
AvaLilac
23241d8777
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 16:04:11 -04:00
AvaLilac
fe3a7c479b
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-24 15:57:40 -04:00
Amelia Frost
02185d005a
use avia_assets as the assets folder 2026-03-24 12:15:18 -07:00
AvaLilac
542023c6a5 Add support for mac os tray 2026-03-24 14:40:27 -04:00
AvaLilac
cbec0b1804 mac os 2026-03-24 14:38:55 -04:00
AvaLilac
982088c96d Update src/native/tray.ts 2026-03-24 11:35:03 -04:00
AvaLilac
77c2337b84 Update src/native/window.ts 2026-03-24 11:34:31 -04:00
AvaLilac
901e416b89 add folder to help change stoats icon 2026-03-24 11:33:52 -04:00
AvaLilac
9db9cd9373 Change the windows title from Stoat to AviaClient To match upcoming updates 2026-03-24 05:33:31 -04:00
AvaLilac
02f1eb08ff Update Stoat for desktop to AviaClient To desktop in hopes of changing the tray's name 2026-03-24 05:22:46 -04:00
AvaLilac
910b356071 Delete test.txt 2026-03-23 20:34:35 -04:00
AvaLilac
63d5e94e6c Add test.txt 2026-03-23 20:34:07 -04:00
Relux
a5327b96cc Delete test file.txt 2026-03-23 20:27:34 -04:00
Relux
bba0bb2c0e This is a test on my local repo 2026-03-23 20:26:28 -04:00
AvaLilac
a926345013
Replace Stoats screenshot.png with one took on aviaclient
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-23 18:05:57 -04:00
AvaLilac
5ac04dff7e
Delete screenshot.png
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-23 18:05:27 -04:00
Amelia Frost
dad8a675e2
Simplify compiling internal plugins 2026-03-23 13:45:12 -07:00
AvaLilac
db0e1b9647
Change Stoat from desktop to AviaClient to desktop
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-23 16:44:08 -04:00
AvaLilac
581cf4fce5
This makes it so AviaClient compiles into a folder named aviaclient. and the exe is also named aviaclient-fordesktop. instead of stoats
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-23 15:39:05 -04:00
Amelia Frost
3d738bfe0e
Simplify loading internal plugins 2026-03-22 17:24:26 -07:00
AvaLilac
dd90d70560
Update LocalPlugins.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 18:57:17 -04:00
AvaLilac
8b81078429
Update main.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 18:52:44 -04:00
AvaLilac
76bbebec1b
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 18:50:40 -04:00
AvaLilac
d0196cd675
Add files via upload
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 18:49:36 -04:00
AvaLilac
5ad6819747
Update themes.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 18:43:27 -04:00
AvaLilac
449c819214
Update pluginsupport.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 18:10:21 -04:00
AvaLilac
e9b04f7dc5
Added Plugins own icon as Extension. To make it more professional
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-22 13:09:29 -04:00
AvaLilac
92ec5e1a9f
Update release-webhook.yml
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 13:12:23 -04:00
AvaLilac
c9165c682e
Update release-please.yml
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 13:11:23 -04:00
AvaLilac
f9c3108590
Update release-please.yml
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 13:07:46 -04:00
AvaLilac
f9a01a3a1a
Update release-please.yml
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 13:05:08 -04:00
AvaLilac
356964d1ee
Update release-please.yml
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 13:01:56 -04:00
AvaLilac
bb93c62e92
Enhance release workflow with Stoat notification
Added notification step to inform Stoat about new releases.

Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 12:58:31 -04:00
AvaLilac
994381411d
Update release-webhook.yml
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 12:46:17 -04:00
AvaLilac
638df6aafd
Inject category script into main window
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 12:01:04 -04:00
AvaLilac
eff6fe57f9
Update pluginsupport.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 12:00:36 -04:00
AvaLilac
45b96855eb
Update themes.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 12:00:22 -04:00
AvaLilac
b2afc5d363
Update inject.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 12:00:00 -04:00
AvaLilac
84ecbc5233
Add files via upload
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 11:59:17 -04:00
AvaLilac
133576411f
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-18 11:58:56 -04:00
AvaLilac
d6932130c4
Add files via upload
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-11 18:44:17 -04:00
AvaLilac
a848275ec8
Delete src/userbadges.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-11 18:43:48 -04:00
AvaLilac
175fb81bcc
Update main.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-11 18:42:52 -04:00
AvaLilac
e20818fb5b
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-11 18:41:53 -04:00
AvaLilac
d94e02e42c
Update userbadges.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-11 07:50:51 -04:00
AvaLilac
61d3ca0fbe
the intergrated code
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-06 11:42:24 -05:00
AvaLilac
8e6ed4bbb2
Had the new themes and Userbadges load into avia
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-06 11:41:46 -05:00
AvaLilac
cbd5f599d9
Added New Themes and intergrated Userbadges
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-03-06 11:41:09 -05:00
AvaLilac
5696875641
Add files via upload
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 19:31:44 -05:00
AvaLilac
a6cd720bc1
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 19:30:36 -05:00
AvaLilac
9712f52e63
Update main.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 19:30:18 -05:00
AvaLilac
5c3b0073e2
Fix
Fixed it so it says copied to clipboard when you click a image/link

Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 08:41:26 -05:00
AvaLilac
58088aa094
fix readme to stop confusion
I thought i broke my source code because i didnt see this option. so i moved it up

Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 08:10:34 -05:00
AvaLilac
b18aacba42
Update forge.config.ts Fix
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 08:06:49 -05:00
AvaLilac
f535e1249c
Update main.ts (fixed
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 08:06:03 -05:00
AvaLilac
cfeb0637e6
FX
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 07:27:05 -05:00
AvaLilac
3256318e1b
PLEase fix
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 07:26:49 -05:00
AvaLilac
fb6a811b63
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 07:17:36 -05:00
AvaLilac
db426339ea
Update main.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 07:13:46 -05:00
AvaLilac
4f4f475ee7
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 07:12:48 -05:00
AvaLilac
c4d24aa88b
Adds Faviorite Gifs/Links/videos support to Stoat
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 06:48:01 -05:00
AvaLilac
d57e155e63
Update main.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 06:47:00 -05:00
AvaLilac
ccf04bfce9
Update forge.config.ts
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-26 06:46:19 -05:00
AvaLilac
21833d3acf
Update user instructions
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 15:38:40 -05:00
AvaLilac
4e00fe9399
ADDED THE CLIENT
Makes sure the client even exists. idk why i forgot to add the MOD ITSELF

Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 12:16:54 -05:00
AvaLilac
c973707d99
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 12:05:33 -05:00
AvaLilac
9a6fbbf8f0
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 12:05:02 -05:00
AvaLilac
1ab4b95570
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 12:03:48 -05:00
AvaLilac
8dfb49c8fe
Update README.md
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 12:03:08 -05:00
AvaLilac
d32fd3b7df
Put inject.js into the same folder as main.js
This is important as without this. Avia client will not load 

Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 10:57:40 -05:00
AvaLilac
fa3bdc7019
Inject Avia client via inject.js
Signed-off-by: AvaLilac <amyshimplays@gmail.com>
2026-02-24 10:56:52 -05:00
42 changed files with 5627 additions and 269 deletions

View file

@ -1,88 +0,0 @@
name: Release Please
on:
push:
branches: [main] # updates/opens the release PR when commits land on main
workflow_dispatch:
permissions:
contents: write
pull-requests: write
id-token: write
concurrency:
group: release-please
cancel-in-progress: true
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.rp.outputs.release_created }}
steps:
- id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_STOAT_RELEASE_APP_ID }}
private-key: ${{ secrets.GH_STOAT_RELEASE_APP_PRIVATE_KEY }}
- id: rp
uses: googleapis/release-please-action@v4
with:
token: ${{ steps.app-token.outputs.token }}
config-file: release-please-config.json
publish-release:
name: Publish App
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout assets
run: git -c submodule."assets".update=checkout submodule update --init assets
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Publish
run: |
pnpm run publish
env:
PLATFORM: ${{ matrix.os }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish macOS x64
if: matrix.os == 'macos-latest'
run: pnpm run publish --arch=x64
env:
PLATFORM: ${{ matrix.os }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Linux arm64
if: matrix.os == 'ubuntu-latest'
run: pnpm run publish --arch=arm64
env:
PLATFORM: ${{ matrix.os }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -23,4 +23,4 @@ jobs:
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$RELEASE_URL\"}"
-d "{\"content\": \"$RELEASE_URL\"}"

View file

@ -1,15 +1,10 @@
<div align="center">
<h1>
Stoat for Desktop
[![Stars](https://img.shields.io/github/stars/stoatchat/for-desktop?style=flat-square&logoColor=white)](https://github.com/stoatchat/for-desktop/stargazers)
[![Forks](https://img.shields.io/github/forks/stoatchat/for-desktop?style=flat-square&logoColor=white)](https://github.com/stoatchat/for-desktop/network/members)
[![Pull Requests](https://img.shields.io/github/issues-pr/stoatchat/for-desktop?style=flat-square&logoColor=white)](https://github.com/stoatchat/for-desktop/pulls)
[![Issues](https://img.shields.io/github/issues/stoatchat/for-desktop?style=flat-square&logoColor=white)](https://github.com/stoatchat/for-desktop/issues)
[![Contributors](https://img.shields.io/github/contributors/stoatchat/for-desktop?style=flat-square&logoColor=white)](https://github.com/stoatchat/for-desktop/graphs/contributors)
[![License](https://img.shields.io/github/license/stoatchat/for-desktop?style=flat-square&logoColor=white)](https://github.com/stoatchat/for-desktop/blob/main/LICENSE)
Avia Client for Desktop
"stoat desktop"
</h1>
Application for Windows, macOS, and Linux.
<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
</div>
<br/>
@ -19,7 +14,8 @@ Application for Windows, macOS, and Linux.
<img src="https://repology.org/badge/vertical-allrepos/stoat-desktop.svg" alt="Packaging status" align="right">
</a>
- All downloads and instructions for Stoat can be found on our [Website](https://stoat.chat/download).
- If you use the Browser you can find FireFox/Chrome/Userscript Builds at [BrowserBuilds](https://github.com/AvaLilac/Ava-Client).
- Though I reccomend you use Userscript if on Chrome Based Browsers. As Plugins do not exist due to browser limits in Extensions. Userscript fine though
## Development Guide
@ -37,18 +33,22 @@ Then proceed to setup:
```bash
# clone the repository
git clone --recursive https://github.com/stoatchat/for-desktop stoat-for-desktop
cd stoat-for-desktop
git clone --recursive https://github.com/AvaLilac/for-desktop aviaclient-for-desktop
# clone the repository (If you are building from developer branch. Which is not always stable)
git clone -b dev --recursive https://github.com/AvaLilac/for-desktop aviaclient-for-desktop
# CD into the directory
cd aviaclient-for-desktop
# install all packages
pnpm i --frozen-lockfile
# start the application
pnpm start
# ... or build the bundle
# update the assets. if you are using stoat's
git -c submodule."assets".update=checkout submodule update --init assets
# build the bundle
pnpm package
# ... or build all distributables
pnpm make
```
Various useful commands for development testing:
@ -71,14 +71,3 @@ pnpm run:nix --force-server=http://localhost:5173
# a better solution would be telling
# Electron Forge where system Electron is
```
### Pulling in Stoat's assets
If you want to pull in Stoat brand assets after pulling, run the following:
```bash
# update the assets
git -c submodule."assets".update=checkout submodule update --init assets
```
Currently, this is required to build, any forks are expected to provide their own assets.

611
about.html Normal file
View file

@ -0,0 +1,611 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>About Aviaclient</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500&family=Google+Sans+Display:wght@400;500&family=Roboto:wght@400;500&display=swap"
rel="stylesheet"
/>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--md-primary: #adc6ff;
--md-primary-container: #003e9c;
--md-on-primary-container: #d8e2ff;
--md-surface: #131318;
--md-surface-container: #1e1e24;
--md-surface-container-high: #28282e;
--md-outline-variant: #46464f;
--md-on-surface: #e4e1e9;
--md-on-surface-variant: #c7c5d0;
--md-secondary: #bec6dc;
--md-secondary-container: #3f4759;
--md-on-secondary-container: #dbe2f9;
--md-tertiary-container: #5c3349;
--md-tertiary: #efb8c8;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
background: var(--md-surface);
color: var(--md-on-surface);
font-family: "Roboto", sans-serif;
font-size: 14px;
display: flex;
flex-direction: column;
}
.topbar {
padding: 14px 24px 10px;
flex-shrink: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.topbar-title {
font-family: "Google Sans", sans-serif;
font-size: 18px;
font-weight: 400;
color: var(--md-on-surface);
}
.layout {
flex: 1 1 0;
min-height: 0;
display: flex;
overflow: hidden;
}
.sidebar {
width: clamp(160px, 26%, 240px);
flex-shrink: 0;
background: var(--md-surface-container);
border-right: 1px solid rgba(255, 255, 255, 0.04);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px 20px;
gap: 14px;
overflow-y: auto;
overflow-x: hidden;
animation: fadeIn 0.25s ease both;
}
.sidebar::-webkit-scrollbar {
width: 4px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
}
.sidebar-text {
text-align: center;
min-width: 0;
width: 100%;
}
.sidebar-text h1 {
font-family: "Google Sans Display", "Google Sans", sans-serif;
font-size: clamp(15px, 2.2vw, 21px);
font-weight: 400;
color: var(--md-on-surface);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-text p {
margin-top: 8px;
font-size: clamp(11px, 1.3vw, 13px);
color: var(--md-on-surface-variant);
line-height: 1.55;
overflow-wrap: break-word;
}
#stoatVersion {
position: absolute;
bottom: 15px;
margin-left: 45px;
font-size: 12px;
color: #888;
}
.sidebar #logo {
position: absolute;
width: 200px;
left: 25px;
top: 75px;
}
.version-chip {
display: inline-flex;
align-items: center;
margin-top: 8px;
padding: 3px 12px;
background: var(--md-secondary-container);
color: var(--md-on-secondary-container);
border-radius: 999px;
font-family: "Google Sans", sans-serif;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.main {
flex: 1 1 0;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 16px 20px 24px;
display: flex;
flex-direction: column;
}
.main::-webkit-scrollbar {
width: 6px;
}
.main::-webkit-scrollbar-track {
background: transparent;
}
.main::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.main::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.18);
}
.section-label {
font-family: "Google Sans", sans-serif;
font-size: 11px;
font-weight: 500;
color: var(--md-primary);
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 12px 4px 6px;
flex-shrink: 0;
}
.section-label:first-child {
padding-top: 4px;
}
.card {
background: var(--md-surface-container);
border-radius: 16px;
margin-bottom: 8px;
flex-shrink: 0;
}
.card {
position: relative;
}
.list-item:first-child {
border-radius: 16px 16px 0 0;
}
.list-item:last-child {
border-radius: 0 0 16px 16px;
}
.list-item:only-child {
border-radius: 16px;
}
.license-icon-row {
border-radius: 16px 16px 0 0;
}
.license-body {
border-radius: 0 0 16px 16px;
}
.list-item {
display: flex;
align-items: center;
padding: 11px 14px;
gap: 12px;
cursor: pointer;
transition: background 0.15s;
position: relative;
text-decoration: none;
color: inherit;
min-width: 0;
}
.list-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.list-item:active {
background: rgba(255, 255, 255, 0.09);
}
.list-item + .list-item::before {
content: "";
position: absolute;
top: 0;
left: 58px;
right: 0;
height: 1px;
background: var(--md-outline-variant);
opacity: 0.35;
}
.item-icon {
width: 36px;
height: 36px;
border-radius: 999px;
background: var(--md-primary-container);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-icon svg {
width: 18px;
height: 18px;
fill: var(--md-primary);
}
.item-icon.secondary {
background: var(--md-secondary-container);
}
.item-icon.secondary svg {
fill: var(--md-secondary);
}
.item-icon.tertiary {
background: var(--md-tertiary-container);
}
.item-icon.tertiary svg {
fill: var(--md-tertiary);
}
.item-text {
flex: 1;
min-width: 0;
}
.item-title {
font-family: "Google Sans", sans-serif;
font-size: 14px;
font-weight: 400;
color: var(--md-on-surface);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-sub {
font-size: 12px;
color: var(--md-on-surface-variant);
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-trail {
flex-shrink: 0;
}
.item-trail svg {
width: 16px;
height: 16px;
fill: var(--md-on-surface-variant);
opacity: 0.45;
display: block;
}
.item-badge {
font-family: "Google Sans", sans-serif;
font-size: 11px;
font-weight: 500;
color: var(--md-on-surface-variant);
background: var(--md-surface-container-high);
border-radius: 999px;
padding: 2px 10px;
white-space: nowrap;
}
.license-icon-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.license-body {
padding: 10px 14px 14px;
}
.license-body p {
font-size: 13px;
color: var(--md-on-surface-variant);
line-height: 1.65;
overflow-wrap: break-word;
}
.license-body a {
color: var(--md-primary);
text-decoration: none;
font-weight: 500;
}
.license-body a:hover {
text-decoration: underline;
}
@media (max-width: 460px) {
.layout {
flex-direction: column;
overflow-y: auto;
}
.sidebar {
width: 100%;
flex-direction: row;
justify-content: flex-start;
padding: 14px 16px;
gap: 14px;
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
overflow: visible;
}
.sidebar-text {
text-align: left;
}
.sidebar-text p {
display: none;
}
.main {
overflow-y: visible;
}
}
.sidebar {
animation: fadeIn 0.25s ease both;
}
.main > * {
animation: slideUp 0.25s ease both;
}
.main > *:nth-child(1) {
animation-delay: 0.06s;
}
.main > *:nth-child(2) {
animation-delay: 0.1s;
}
.main > *:nth-child(3) {
animation-delay: 0.13s;
}
.main > *:nth-child(4) {
animation-delay: 0.16s;
}
.main > *:nth-child(5) {
animation-delay: 0.19s;
}
.main > *:nth-child(6) {
animation-delay: 0.22s;
}
.main > *:nth-child(7) {
animation-delay: 0.25s;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
</head>
<body>
<div class="topbar">
<span class="topbar-title">About</span>
</div>
<div class="layout">
<div class="sidebar">
<img id="logo" title="Aurora" src="avia_assets/icon.png" />
<div class="sidebar-text">
<h1>Aviaclient</h1>
<div id="aviaVersion" class="version-chip">TBD</div>
<p>
A custom desktop client with enhancements and additional features.
</p>
<div id="stoatVersion">Based on Stoat</div>
</div>
</div>
<div class="main">
<div class="section-label">Links</div>
<div class="card">
<a
class="list-item"
href="https://github.com/AvaLilac/for-desktop"
target="_blank"
>
<div class="item-icon">
<svg viewBox="0 0 24 24">
<path
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"
/>
</svg>
</div>
<div class="item-text">
<div class="item-title">Source code</div>
<div class="item-sub">github.com/AvaLilac/for-desktop</div>
</div>
<div class="item-trail">
<svg viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" />
</svg>
</div>
</a>
<a
class="list-item"
href="https://github.com/AvaLilac/for-desktop/issues"
target="_blank"
>
<div class="item-icon secondary">
<svg viewBox="0 0 24 24">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
/>
</svg>
</div>
<div class="item-text">
<div class="item-title">Issues</div>
<div class="item-sub">Report bugs or request features</div>
</div>
<div class="item-trail">
<svg viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" />
</svg>
</div>
</a>
<a
class="list-item"
href="https://github.com/AvaLilac/for-desktop/tree/dev"
target="_blank"
>
<div class="item-icon tertiary">
<svg viewBox="0 0 24 24">
<path
d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"
/>
</svg>
</div>
<div class="item-text">
<div class="item-title">Dev branch</div>
<div class="item-sub">Latest development changes</div>
</div>
<div class="item-trail">
<svg viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" />
</svg>
</div>
</a>
</div>
<div class="section-label">License</div>
<div class="card">
<div class="license-icon-row">
<div class="item-icon tertiary">
<svg viewBox="0 0 24 24">
<path
d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"
/>
</svg>
</div>
<div class="item-text">
<div class="item-title">GNU AGPL v3.0</div>
</div>
</div>
<div class="license-body">
<p>
Licensed under the
<a
href="https://www.gnu.org/licenses/agpl-3.0.txt"
target="_blank"
>GNU Affero General Public License v3.0</a
>. You are free to use, modify, and distribute this software under
the same license.
</p>
</div>
</div>
<div class="section-label">Open source</div>
<div class="card">
<a
class="list-item"
href="https://github.com/electron/electron"
target="_blank"
>
<div class="item-icon">
<svg viewBox="0 0 24 24">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"
/>
</svg>
</div>
<div class="item-text">
<div class="item-title">Electron</div>
<div class="item-sub">Desktop runtime framework</div>
</div>
<div class="item-trail">
<span class="item-badge">runtime</span>
</div>
</a>
<a
class="list-item"
href="https://github.com/electron-userland/electron-builder"
target="_blank"
>
<div class="item-icon secondary">
<svg viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
</div>
<div class="item-text">
<div class="item-title">Electron Forge</div>
<div class="item-sub">Packaging and distribution</div>
</div>
<div class="item-trail">
<span class="item-badge">packaging</span>
</div>
</a>
<a
class="list-item"
href="https://github.com/discordjs/RPC"
target="_blank"
>
<div class="item-icon tertiary">
<svg viewBox="0 0 24 24">
<path
d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"
/>
</svg>
</div>
<div class="item-text">
<div class="item-title">discord-rpc</div>
<div class="item-sub">Discord Rich Presence integration</div>
</div>
<div class="item-trail"><span class="item-badge">rpc</span></div>
</a>
</div>
</div>
</div>
<script>
window.addEventListener("load", () => {
const aviaElem = document.getElementById("aviaVersion");
if (aviaElem) {
aviaElem.textContent = window.native.versions.aviaClient();
}
const stoatElem = document.getElementById("stoatVersion");
if (stoatElem) {
stoatElem.textContent = `Based on Stoat ${window.native.versions.desktop()}`;
}
});
</script>
</body>
</html>

BIN
avia_assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View file

@ -0,0 +1,75 @@
{
"fill-specializations" : [
{
"value" : {
"solid" : "display-p3:0.33912,0.12908,0.93430,1.00000"
}
},
{
"appearance" : "dark",
"value" : {
"automatic-gradient" : "display-p3:0.15333,0.05837,0.42244,1.00000",
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.5,
"y" : 0.7
}
}
}
}
],
"groups" : [
{
"layers" : [
{
"blend-mode-specializations" : [
{
"value" : "normal"
},
{
"appearance" : "tinted",
"value" : "normal"
}
],
"fill-specializations" : [
{
"value" : "automatic"
},
{
"appearance" : "tinted",
"value" : "none"
}
],
"glass" : false,
"image-name" : "IMG_0248.PNG",
"name" : "IMG_0248",
"position" : {
"scale" : 0.25,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

BIN
avia_assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

29
avia_core/ButtonFix.js Normal file
View file

@ -0,0 +1,29 @@
(function () {
if (window.__BUTTON_FIX__) return;
window.__BUTTON_FIX__ = true;
function uninjectButton(button){
if(button){
button.parentElement.removeChild(button)
}
}
const observer = new MutationObserver(()=>{
let balls = [];
document.querySelectorAll('div[class=\'flex-sh_0 d_flex ai_end jc_center w_42px\']').forEach(element=>{
if(element.id?.includes('avia')){
balls.push(element)
}
})
const gifSpan = [...document.querySelectorAll("span.material-symbols-outlined")]
.find(s => s.textContent.trim() === "gif");
if(!gifSpan){
balls.forEach(element=>{
uninjectButton(element)
})
}
});
observer.observe(document.documentElement, {childList: true, subtree: true })
})();

619
avia_core/LocalPlugins.js Normal file
View file

@ -0,0 +1,619 @@
(function () {
if (window.__AVIA_LOCAL_PLUGINS_LOADED__) return;
window.__AVIA_LOCAL_PLUGINS_LOADED__ = true;
const STORAGE_KEY = "avia_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 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 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 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();
});
getLocalPlugins().forEach(plugin => {
if (plugin.enabled) runLocalPlugin(plugin);
});
preloadMonaco();
})();

139
avia_core/LoginWithToken.js Normal file
View file

@ -0,0 +1,139 @@
(function () {
if (window.__LOGIN_WITH_TOKEN__) return;
window.__LOGIN_WITH_TOKEN__ = true;
async function loginWithToken(token) {
const res = await fetch('https://stoat.chat/api/users/@me', {
headers: { 'x-session-token': token }
});
if (!res.ok) throw new Error('Invalid token');
const user = await res.json();
const db = await new Promise((resolve, reject) => {
const r = indexedDB.open('localforage');
r.onsuccess = () => resolve(r.result);
r.onerror = () => reject(r.error);
});
const tx = db.transaction('keyvaluepairs', 'readwrite');
await new Promise((resolve, reject) => {
const r = tx.objectStore('keyvaluepairs').put({
session: {
_id: user._id,
token: token,
userId: user._id,
valid: true
}
}, 'auth');
r.onsuccess = () => resolve();
r.onerror = () => reject(r.error);
});
location.reload();
}
function openTokenDialog() {
const backdrop = document.createElement('div');
backdrop.className = 'top_0 left_0 right_0 bottom_0 pos_fixed z_100 max-h_100% d_grid us_none place-items_center pointer-events_all anim-n_scrimFadeIn anim-dur_0.1s anim-fm_forwards trs_var(--transitions-medium)_all p_80px ov-y_auto';
backdrop.style.cssText = '--background: rgba(0, 0, 0, 0.6);';
backdrop.innerHTML = `
<div style="opacity: 1; --motion-translateY: 0px; transform: translateY(var(--motion-translateY));">
<div class="p_24px min-w_280px max-w_560px bdr_28px d_flex flex-d_column c_var(--md-sys-color-on-surface) bg_var(--md-sys-color-surface-container-high)">
<span class="lh_2rem fs_1.5rem ls_0 fw_400 mbe_16px">Login With Token</span>
<div class="c_var(--md-sys-color-on-surface-variant) lh_1.25rem fs_0.875rem ls_0.015625rem fw_400">
<div class="d_flex flex-d_column flex-g_initial m_0 ai_initial jc_initial gap_var(--gap-md)">
<mdui-text-field id="lwt-token-input" variant="filled" type="password" name="token" required label="Session Token"></mdui-text-field>
</div>
</div>
<div class="gap_8px d_flex jc_end mbs_24px">
<button id="lwt-close-btn" type="button" class="lh_1.25rem fs_0.875rem ls_0.015625rem fw_400 pos_relative px_16px flex-sh_0 d_flex ai_center jc_center ff_inherit cursor_pointer bd_none trs_var(--transitions-medium)_all c_var(--color) fill_var(--color) h_40px bdr_var(--borderRadius-full) --color_var(--md-sys-color-primary)">
<md-ripple aria-hidden="true"></md-ripple>Close
</button>
<button id="lwt-login-btn" type="button" class="lh_1.25rem fs_0.875rem ls_0.015625rem fw_400 pos_relative px_16px flex-sh_0 d_flex ai_center jc_center ff_inherit cursor_pointer bd_none trs_var(--transitions-medium)_all c_var(--color) fill_var(--color) h_40px bdr_var(--borderRadius-full) --color_var(--md-sys-color-on-primary) bg_var(--md-sys-color-primary)">
<md-ripple aria-hidden="true"></md-ripple>Login
</button>
</div>
</div>
</div>
`;
document.body.appendChild(backdrop);
const closeBtn = backdrop.querySelector('#lwt-close-btn');
const loginBtn = backdrop.querySelector('#lwt-login-btn');
const tokenInput = backdrop.querySelector('#lwt-token-input');
function close() { backdrop.remove(); }
function setLoading(loading) {
loginBtn.disabled = loading;
loginBtn.style.cursor = loading ? 'not-allowed' : 'pointer';
const ripple = loginBtn.querySelector('md-ripple');
loginBtn.textContent = loading ? 'Logging in…' : 'Login';
if (ripple) loginBtn.prepend(ripple);
}
function setError(msg) {
loginBtn.disabled = false;
loginBtn.style.cursor = 'pointer';
const ripple = loginBtn.querySelector('md-ripple');
loginBtn.textContent = msg;
if (ripple) loginBtn.prepend(ripple);
setTimeout(() => {
loginBtn.textContent = 'Login';
if (ripple) loginBtn.prepend(ripple);
}, 2000);
}
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
closeBtn.addEventListener('click', close);
loginBtn.addEventListener('click', async () => {
const token = tokenInput.value?.trim();
if (!token) {
setError('Enter a token!');
return;
}
setLoading(true);
try {
await loginWithToken(token);
} catch (err) {
setError('Invalid token!');
}
});
}
function injectLoginButton() {
const signUpBtn = [...document.querySelectorAll('button')]
.find(b => b.textContent.trim() === 'Sign Up');
if (!signUpBtn) return;
const parent = signUpBtn.parentElement;
if (parent.querySelector('[data-lwt-btn]')) return;
const clone = signUpBtn.cloneNode(false);
clone.dataset.lwtBtn = 'true';
clone.textContent = 'Login With Token';
const ripple = document.createElement('md-ripple');
ripple.setAttribute('aria-hidden', 'true');
clone.prepend(ripple);
clone.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
openTokenDialog();
});
signUpBtn.insertAdjacentElement('afterend', clone);
}
let debounceTimer = null;
new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(injectLoginButton, 150);
}).observe(document.body, { childList: true, subtree: true });
injectLoginButton();
})();

View file

@ -0,0 +1,34 @@
(function(){
if(window.__AVIA_CATEGORY_SETTINGS__) return;
window.__AVIA_CATEGORY_SETTINGS__ = true;
function inject(){
if(document.getElementById('avia-cloned-settings')) return;
const spans = [...document.querySelectorAll('span')];
const target = spans.find(s => s.textContent.trim() === "User Settings");
if(!target) return;
const container = target.closest('.d_flex.flex-d_column');
if(!container) return;
const clone = container.cloneNode(true);
clone.id = "avia-cloned-settings";
const header = clone.querySelector('span');
if(header) header.textContent = "AVIA CLIENT SETTINGS";
const list = clone.querySelector('.d_flex.flex-d_column.gap_var\\(--gap-s\\)');
if(list) list.innerHTML = "";
container.parentNode.insertBefore(clone, container.nextSibling);
}
new MutationObserver(() => {
inject();
}).observe(document.body, { childList: true, subtree: true });
inject();
})();

View file

@ -0,0 +1,40 @@
(function () {
if (window.__AVIA_DESKTOP_PATCH__) return;
window.__AVIA_DESKTOP_PATCH__ = true;
function patchButton() {
document
.querySelectorAll("a.pos_relative.gap_16px.p_13px")
.forEach((el) => {
if (el.dataset.aviaPatched) return;
const textContainer = el.querySelector(
"div.d_flex.flex-g_1.flex-d_column",
);
if (!textContainer) return;
const nameDiv = textContainer.querySelector("div");
const versionSpan = textContainer.querySelector("span.lh_1rem");
if (!nameDiv || !versionSpan) return;
if (!nameDiv.textContent.includes("Stoat for Desktop")) return;
if (!versionSpan.textContent.includes("Version:")) return;
const aviaVersion = window.native.versions.aviaClient();
const stoatVersion = window.native.versions.desktop();
el.dataset.aviaPatched = "true";
nameDiv.textContent = "Sanctum Desktop";
versionSpan.textContent = `Version ${aviaVersion} (Based on Stoat ${stoatVersion})`;
textContainer.style.whiteSpace = "normal";
textContainer.style.overflow = "visible";
});
}
const observer = new MutationObserver(patchButton);
observer.observe(document.body, { childList: true, subtree: true });
patchButton();
})();

349
avia_core/aviafavsystem.js Normal file
View file

@ -0,0 +1,349 @@
(function () {
if (window.__AVIA_FAVORITES_LOADED__) return;
window.__AVIA_FAVORITES_LOADED__ = true;
const STORAGE_KEY = "avia_favorites";
const getFavorites = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
const setFavorites = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
function extractYouTubeID(url) {
const reg = /(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^&?/]+)/;
const match = url.match(reg);
return match ? match[1] : null;
}
function toggleFavoritesPanel() {
let panel = document.getElementById("avia-favorites-panel");
if (panel) {
panel.style.display = panel.style.display === "none" ? "flex" : "none";
return;
}
panel = document.createElement("div");
panel.id = "avia-favorites-panel";
Object.assign(panel.style, {
position: "fixed",
bottom: "40px",
right: "40px",
width: "640px",
height: "580px",
background: "#1e1e1e",
color: "#fff",
borderRadius: "20px",
boxShadow: "0 12px 35px rgba(0,0,0,0.45)",
zIndex: 999999,
display: "flex",
flexDirection: "column",
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.08)"
});
const header = document.createElement("div");
header.textContent = "Favorites";
Object.assign(header.style, {
padding: "18px",
fontWeight: "600",
fontSize: "16px",
background: "rgba(255,255,255,0.04)",
borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor: "move",
position: "relative",
userSelect: "none"
});
const close = document.createElement("div");
close.textContent = "✕";
Object.assign(close.style, {
position: "absolute",
right: "18px",
top: "16px",
cursor: "pointer"
});
close.onclick = () => panel.style.display = "none";
header.appendChild(close);
const inputRow = document.createElement("div");
Object.assign(inputRow.style, {
display: "flex",
gap: "8px",
padding: "14px 18px"
});
const urlInput = document.createElement("input");
urlInput.placeholder = "Paste link...";
Object.assign(urlInput.style, {
flex: "2",
padding: "10px",
borderRadius: "10px",
border: "none",
outline: "none"
});
const titleInput = document.createElement("input");
titleInput.placeholder = "Optional title...";
Object.assign(titleInput.style, {
flex: "1",
padding: "10px",
borderRadius: "10px",
border: "none",
outline: "none"
});
const addBtn = document.createElement("button");
addBtn.textContent = "Add";
Object.assign(addBtn.style, {
padding: "10px 16px",
borderRadius: "10px",
border: "none",
cursor: "pointer"
});
inputRow.appendChild(urlInput);
inputRow.appendChild(titleInput);
inputRow.appendChild(addBtn);
const grid = document.createElement("div");
Object.assign(grid.style, {
flex: "1",
minHeight: "0",
overflowY: "auto",
padding: "18px",
display: "grid",
gridTemplateColumns: "repeat(auto-fill, 120px)",
gap: "14px",
alignContent: "start"
});
panel.appendChild(header);
panel.appendChild(inputRow);
panel.appendChild(grid);
document.body.appendChild(panel);
let isDragging = false, offsetX, offsetY;
header.addEventListener("mousedown", e => {
isDragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
});
document.addEventListener("mouseup", () => isDragging = false);
document.addEventListener("mousemove", e => {
if (!isDragging) return;
panel.style.left = (e.clientX - offsetX) + "px";
panel.style.top = (e.clientY - offsetY) + "px";
panel.style.right = "auto";
panel.style.bottom = "auto";
});
function showToast(card) {
const toast = document.createElement("div");
toast.textContent = "Copied to clipboard";
Object.assign(toast.style, {
position: "absolute",
bottom: "6px",
left: "50%",
transform: "translateX(-50%)",
background: "rgba(0,0,0,0.85)",
padding: "6px 10px",
borderRadius: "8px",
fontSize: "11px",
opacity: "0",
transition: "opacity 0.2s",
pointerEvents: "none"
});
card.appendChild(toast);
requestAnimationFrame(() => toast.style.opacity = "1");
setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 200);
}, 2000);
}
function fallbackCopy(text) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try { document.execCommand("copy"); } catch {}
document.body.removeChild(textarea);
}
function render() {
grid.innerHTML = "";
const favorites = getFavorites();
favorites.forEach(item => {
const card = document.createElement("div");
Object.assign(card.style, {
position: "relative",
width: "120px",
height: "120px",
borderRadius: "14px",
overflow: "hidden",
background: "rgba(255,255,255,0.05)",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center"
});
const remove = document.createElement("div");
remove.textContent = "✕";
Object.assign(remove.style, {
position: "absolute",
top: "6px",
right: "8px",
fontSize: "12px",
cursor: "pointer",
background: "rgba(0,0,0,0.6)",
padding: "2px 6px",
borderRadius: "6px",
zIndex: 2
});
remove.onclick = (e) => {
e.stopPropagation();
setFavorites(favorites.filter(f => f.url !== item.url));
render();
};
card.appendChild(remove);
let mediaAdded = false;
const ytID = extractYouTubeID(item.url);
if (ytID) {
const img = new Image();
img.src = `https://img.youtube.com/vi/${ytID}/hqdefault.jpg`;
Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" });
card.appendChild(img);
mediaAdded = true;
}
if (!mediaAdded) {
const ext = item.url.split(".").pop().split("?")[0].toLowerCase();
const isVideo = ["mp4","webm","mov","gifv"].includes(ext);
if (isVideo) {
const video = document.createElement("video");
video.src = item.url.replace(".gifv",".mp4");
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
Object.assign(video.style, { width:"100%", height:"100%", objectFit:"cover" });
video.onerror = fallback;
card.appendChild(video);
} else {
const img = new Image();
img.src = item.url;
Object.assign(img.style, { width:"100%", height:"100%", objectFit:"cover" });
img.onerror = fallback;
card.appendChild(img);
}
}
function fallback() {
card.innerHTML = "";
card.appendChild(remove);
const text = document.createElement("div");
text.textContent = item.title || item.url;
Object.assign(text.style, {
padding:"8px",
fontSize:"11px",
textAlign:"center",
wordBreak:"break-word"
});
card.appendChild(text);
}
if (item.title) {
const titleOverlay = document.createElement("div");
titleOverlay.textContent = item.title;
Object.assign(titleOverlay.style, {
position:"absolute",
bottom:"0",
width:"100%",
background:"rgba(0,0,0,0.6)",
fontSize:"11px",
padding:"4px",
textAlign:"center",
whiteSpace:"nowrap",
overflow:"hidden",
textOverflow:"ellipsis"
});
card.appendChild(titleOverlay);
}
card.onclick = () => {
const doToast = () => showToast(card);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(item.url)
.then(doToast)
.catch(() => {
fallbackCopy(item.url);
doToast();
});
} else {
fallbackCopy(item.url);
doToast();
}
};
grid.appendChild(card);
});
}
addBtn.onclick = () => {
const url = urlInput.value.trim();
const title = titleInput.value.trim();
if (!url) return;
const favorites = getFavorites();
if (favorites.some(f => f.url === url)) return;
favorites.push({ url, title, addedAt: Date.now() });
setFavorites(favorites);
urlInput.value = "";
titleInput.value = "";
render();
};
render();
}
function injectButton() {
if (document.getElementById("avia-favorites-btn")) return;
const gifSpan = [...document.querySelectorAll("span.material-symbols-outlined")]
.find(s => s.textContent.trim() === "gif");
if (!gifSpan) return;
const wrapper = gifSpan.closest("div.flex-sh_0");
if (!wrapper) return;
const clone = wrapper.cloneNode(true);
clone.id = "avia-favorites-btn";
clone.querySelector("span.material-symbols-outlined").textContent = "star";
clone.querySelector("button").onclick = toggleFavoritesPanel;
wrapper.parentElement.insertBefore(clone, wrapper.nextSibling);
}
new MutationObserver(injectButton)
.observe(document.body, { childList: true, subtree: true });
injectButton();
})();

34
avia_core/aviaversion.js Normal file
View file

@ -0,0 +1,34 @@
(function () {
if (window.__AVIA_VERSION_PATCH__) return;
window.__AVIA_VERSION_PATCH__ = true;
function patchVersion() {
document
.querySelectorAll("span.lh_1rem.fs_0\\.75rem.ls_0\\.03125rem.fw_500")
.forEach((el) => {
if (el.dataset.aviaPatched) return;
if (!el.textContent.trim().startsWith("Stoat for Desktop")) return;
const stoatVersion = window.native.versions.desktop();
el.dataset.aviaPatched = "true";
el.innerHTML = `
Sanctum Desktop<br>
<span style="font-size:10px;opacity:0.7;">
Based on Stoat ${stoatVersion}
</span>
`;
});
}
const observer = new MutationObserver(patchVersion);
observer.observe(document.body, {
childList: true,
subtree: true,
});
patchVersion();
})();

175
avia_core/clientBackup.js Normal file
View file

@ -0,0 +1,175 @@
(function () {
if (window.__clientBackup) return;
window.__clientBackup = true;
const TARGET_TEXT = "Spellchecker";
const CLONE_KEY = "data-lsbackup-cloned";
function exportLS() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "localstorage-backup.json";
a.click();
URL.revokeObjectURL(url);
}
function importLS(file, onDone) {
const reader = new FileReader();
reader.onload = e => {
try {
const data = JSON.parse(e.target.result);
let count = 0;
for (const [key, value] of Object.entries(data)) {
localStorage.setItem(key, value);
count++;
}
onDone(null, count);
} catch (err) {
onDone(err);
}
};
reader.readAsText(file);
}
function buildPanel() {
const panel = document.createElement("div");
panel.style.cssText = `
display: none;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
background: var(--md-sys-color-surface-container-highest);
border: 1px solid var(--md-sys-color-outline-variant);
font-size: 12px;
color: var(--md-sys-color-on-surface);
`;
const btnStyle = `
padding: 5px 12px;
border-radius: 4px;
border: none;
font-size: 11px;
font-weight: 600;
cursor: pointer;
`;
const status = document.createElement("span");
status.style.cssText = "font-size: 11px; opacity: 0.7; min-height: 14px;";
const exportBtn = document.createElement("button");
exportBtn.textContent = "⬇ Export localStorage";
exportBtn.style.cssText = btnStyle + `
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
`;
exportBtn.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
exportLS();
status.textContent = `✓ Exported ${localStorage.length} keys`;
});
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".json";
fileInput.style.cssText = "display: none;";
fileInput.addEventListener("change", e => {
const file = e.target.files[0];
if (!file) return;
importLS(file, (err, count) => {
if (err) {
status.textContent = "✗ Invalid JSON file";
} else {
status.textContent = `✓ Imported ${count} keys`;
}
fileInput.value = "";
});
});
const importBtn = document.createElement("button");
importBtn.textContent = "⬆ Import localStorage";
importBtn.style.cssText = btnStyle + `
background: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface);
border: 1px solid var(--md-sys-color-outline-variant);
`;
importBtn.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
panel.appendChild(exportBtn);
panel.appendChild(importBtn);
panel.appendChild(fileInput);
panel.appendChild(status);
return panel;
}
function tryInject() {
document.querySelectorAll("a.pos_relative").forEach(btn => {
if (
btn.hasAttribute(CLONE_KEY) ||
btn.hasAttribute("data-lsbackup-entry") ||
!btn.innerText.includes(TARGET_TEXT)
) return;
btn.setAttribute(CLONE_KEY, "true");
const clone = btn.cloneNode(true);
clone.removeAttribute(CLONE_KEY);
clone.setAttribute("data-lsbackup-entry", "true");
const title = clone.querySelector("div.d_flex.flex-g_1.flex-d_column > div");
if (title) title.textContent = "Sanctum Backup";
const desc = clone.querySelector("div.d_flex.flex-g_1.flex-d_column > span");
if (desc) desc.textContent = "Backup or Restore all client data";
const iconBtn = document.createElement("div");
iconBtn.title = "LocalStorage Backup";
iconBtn.style.cssText = "cursor: pointer; z-index: 10; flex-shrink: 0;";
iconBtn.innerHTML = `
<div class="fill_var(--md-sys-color-on-surface) bg_var(--md-sys-color-surface-dim) w_36px h_36px d_flex flex-sh_0 ai_center jc_center bdr_var(--borderRadius-full)">
<span aria-hidden="true" class="material-symbols-outlined fs_inherit fw_undefined!" style="display: block; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0;">database</span>
</div>
`;
const existingIcon = clone.querySelector("div.fill_var\\(--md-sys-color-on-surface\\)");
if (existingIcon) {
existingIcon.replaceWith(iconBtn);
} else {
clone.prepend(iconBtn);
}
clone.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
panel.style.display = panel.style.display === "flex" ? "none" : "flex";
});
const wrapper = document.createElement("div");
wrapper.style.cssText = "display: flex; flex-direction: column;";
const panel = buildPanel();
wrapper.appendChild(clone);
wrapper.appendChild(panel);
btn.parentNode.insertBefore(wrapper, btn.nextSibling);
});
}
tryInject();
const observer = new MutationObserver(() => tryInject());
observer.observe(document.body, { childList: true, subtree: true });
})();

View file

@ -0,0 +1,57 @@
(function () {
if (window.__customFrameNativeMenu) return;
window.__customFrameNativeMenu = true;
function toggleCheckbox(elem, toggle) {
const checkbox = elem.querySelector("mdui-checkbox");
if (!checkbox) return;
if (toggle) {
checkbox.setAttribute('checked', '');
checkbox.setAttribute('value', 'on');
} else {
checkbox.removeAttribute('checked');
checkbox.setAttribute('value', 'off');
}
}
function initCFNM() {
let elem = document.querySelector("#floating div:not(:empty) div.will-change_transform.flex_1_1_800px div:has(> a) > a:last-child");
if (!elem) { return; }
let title = elem.querySelector("div.flex-g_1 > div");
if (!title || title.textContent.trim() !== "Custom window frame") { return; }
let desc = elem.querySelector("div.flex-g_1 > span");
if (!desc || desc.textContent.trim() !== "Let Stoat use its own custom titlebar.") { return; }
var newElem = elem.cloneNode(true);
let nTitle = newElem.querySelector("div.flex-g_1 > div");
let nDesc = newElem.querySelector("div.flex-g_1 > span");
if (!nTitle || !nDesc) { newElem = null; return; }
nTitle.textContent = "Native menu on custom window frame";
nDesc.textContent = "Use the system's native menu on the custom window frame.";
let config = window.desktopConfig.get();
toggleCheckbox(newElem, config.customFrameNativeMenu);
newElem.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
let config = window.desktopConfig.get();
config.customFrameNativeMenu = !config.customFrameNativeMenu;
window.desktopConfig.set(config);
toggleCheckbox(newElem, config.customFrameNativeMenu);
});
elem.parentNode.appendChild(newElem);
}
initCFNM();
const observer = new MutationObserver(() => initCFNM());
observer.observe(document.body, { childList: true, subtree: true });
})();

View file

@ -0,0 +1,66 @@
(function () {
if (window.__disableTrayClick) return;
window.__disableTrayClick = true;
function toggleCheckbox(elem, value) {
const checkbox = elem.querySelector("mdui-checkbox");
if (!checkbox) return;
if (value) {
checkbox.setAttribute("checked", "");
checkbox.setAttribute("value", "on");
} else {
checkbox.removeAttribute("checked");
checkbox.setAttribute("value", "off");
}
}
function createButton(baseElem) {
const newElem = baseElem.cloneNode(true);
newElem.setAttribute("data-disable-tray-click", "true");
const title = newElem.querySelector("div.d_flex.flex-g_1 > div");
const desc = newElem.querySelector("div.d_flex.flex-g_1 > span");
const icon = newElem.querySelector("div.w_36px span.material-symbols-outlined");
if (title) title.textContent = "Disable Tray Icon Click";
if (desc) desc.textContent = "Prevents tray icon from toggling the app window.";
if (icon) icon.textContent = "block";
let config = window.desktopConfig.get();
toggleCheckbox(newElem, config.disableTrayClick);
newElem.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
let config = window.desktopConfig.get();
config.disableTrayClick = !config.disableTrayClick;
window.desktopConfig.set(config);
toggleCheckbox(newElem, config.disableTrayClick);
});
return newElem;
}
function injectButton() {
const base = Array.from(document.querySelectorAll("a")).find((e) => {
const t = e.querySelector("div.d_flex.flex-g_1 > div");
return t && t.textContent.trim() === "Discord RPC";
});
if (!base) return;
if (document.querySelector("[data-disable-tray-click]")) return;
const newButton = createButton(base);
base.parentNode.appendChild(newButton);
}
injectButton();
const observer = new MutationObserver(() => injectButton());
observer.observe(document.body, { childList: true, subtree: true });
})();

238
avia_core/headliner.js Normal file
View file

@ -0,0 +1,238 @@
(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: "Stoat V 1.0.0 - Sanctum",
left: "32",
top: "56",
fontSize: "15",
fontWeight: "700"
};
function loadSettings() {
try {
return JSON.parse(localStorage.getItem("headlinerSettings")) || { ...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;
}
.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);
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 Stoat 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: &quot;FILL&quot; 0, &quot;wght&quot; 400, &quot;GRAD&quot; 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 });
})();

361
avia_core/inject.js Normal file
View file

@ -0,0 +1,361 @@
(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();
})();

575
avia_core/pluginsupport.js Normal file
View file

@ -0,0 +1,575 @@
(function () {
if (window.__AVIA_PLUGINS_LOADED__) return;
window.__AVIA_PLUGINS_LOADED__ = true;
const STORAGE_KEY = "avia_plugins";
const runningPlugins = {};
const pluginErrors = {};
const injectionQueue = [];
const getPlugins = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
const setPlugins = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
function normalizePluginUrl(url) {
try {
const u = new URL(url);
if (u.hostname === "github.com") {
const m = u.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
if (m) {
return `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3]}/${m[4]}`;
}
return url;
}
if (u.hostname === "raw.githubusercontent.com") return url;
if (u.hostname === "raw.codeberg.page") return url;
if (u.hostname === "codeberg.org") {
const parts = u.pathname.split("/").filter(Boolean);
if (parts.length >= 5 && (parts[2] === "raw" || parts[2] === "src")) {
const user = parts[0];
const repo = parts[1];
const branchName = parts[3] === "branch" || parts[3] === "commit" || parts[3] === "tag"
? parts[4]
: parts[3];
const fileStart = parts[3] === "branch" || parts[3] === "commit" || parts[3] === "tag"
? 5
: 4;
const filePath = parts.slice(fileStart).join("/");
return `https://raw.codeberg.page/${user}/${repo}/@${branchName}/${filePath}`;
}
if (parts.length >= 4 && parts[2] === "raw") {
const user = parts[0];
const repo = parts[1];
const branchName = parts[3];
const filePath = parts.slice(4).join("/");
return `https://raw.codeberg.page/${user}/${repo}/@${branchName}/${filePath}`;
}
}
} catch (_) {
}
return url;
}
async function processQueue() {
if (processQueue.running) return;
processQueue.running = true;
while (injectionQueue.length) {
const { plugin, force } = injectionQueue.shift();
await loadPluginInternal(plugin, force);
}
processQueue.running = false;
}
function queuePlugin(plugin, force = false) {
injectionQueue.push({ plugin, force });
processQueue();
}
async function loadPluginInternal(plugin, force = false) {
if (runningPlugins[plugin.url] && !force) return;
if (force) stopPlugin(plugin);
try {
const fetchUrl = normalizePluginUrl(plugin.url);
const res = await fetch(fetchUrl);
if (!res.ok) throw new Error("Fetch failed");
const code = await res.text();
delete pluginErrors[plugin.url];
const script = document.createElement("script");
script.textContent = code;
script.dataset.pluginUrl = plugin.url;
document.body.appendChild(script);
runningPlugins[plugin.url] = script;
} catch {
pluginErrors[plugin.url] = true;
}
renderPanel();
}
function stopPlugin(plugin) {
const script = runningPlugins[plugin.url];
if (!script) return;
script.remove();
delete runningPlugins[plugin.url];
delete pluginErrors[plugin.url];
renderPanel();
}
function preloadMonaco() {
return new Promise(resolve => {
if (window.monaco) return resolve();
const loader = document.createElement("script");
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
loader.onload = function () {
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
require(["vs/editor/editor.main"], () => resolve());
};
document.head.appendChild(loader);
});
}
async function openViewerPanel(plugin) {
await preloadMonaco();
const existing = document.getElementById("avia-plugin-viewer-panel");
if (existing) existing.remove();
const panel = document.createElement("div");
panel.id = "avia-plugin-viewer-panel";
Object.assign(panel.style, {
position: "fixed",
bottom: "24px",
left: "24px",
width: "700px",
height: "480px",
background: "var(--md-sys-color-surface, #1e1e1e)",
borderRadius: "16px",
boxShadow: "0 8px 28px rgba(0,0,0,0.45)",
zIndex: "9999999",
display: "flex",
flexDirection: "column",
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.08)",
backdropFilter: "blur(12px)",
color: "#fff"
});
const header = document.createElement("div");
Object.assign(header.style, {
padding: "14px 16px",
fontWeight: "600",
fontSize: "14px",
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor: "move",
display: "flex",
alignItems: "center",
gap: "10px",
flex: "0 0 auto"
});
const titleText = document.createElement("span");
titleText.textContent = `Viewing: ${plugin.name}`;
titleText.style.flex = "1";
const readOnlyBadge = document.createElement("span");
readOnlyBadge.textContent = "READ ONLY";
Object.assign(readOnlyBadge.style, {
fontSize: "10px",
fontWeight: "700",
letterSpacing: "0.08em",
padding: "2px 8px",
borderRadius: "20px",
background: "rgba(255,180,0,0.15)",
color: "#ffb400",
border: "1px solid rgba(255,180,0,0.3)"
});
const closeBtn = document.createElement("div");
closeBtn.textContent = "✕";
Object.assign(closeBtn.style, {
cursor: "pointer",
opacity: "0.6",
fontSize: "15px",
lineHeight: "1",
padding: "2px 4px"
});
closeBtn.onmouseenter = () => closeBtn.style.opacity = "1";
closeBtn.onmouseleave = () => closeBtn.style.opacity = "0.6";
closeBtn.onclick = () => panel.remove();
header.appendChild(titleText);
header.appendChild(readOnlyBadge);
header.appendChild(closeBtn);
const urlBar = document.createElement("div");
Object.assign(urlBar.style, {
padding: "8px 16px",
borderBottom: "1px solid rgba(255,255,255,0.06)",
fontSize: "11px",
color: "rgba(255,255,255,0.35)",
fontFamily: "monospace",
background: "rgba(0,0,0,0.15)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: "0 0 auto"
});
urlBar.textContent = plugin.url;
urlBar.title = plugin.url;
const editorContainer = document.createElement("div");
editorContainer.style.flex = "1";
editorContainer.style.overflow = "hidden";
const loadingMsg = document.createElement("div");
Object.assign(loadingMsg.style, {
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
opacity: "0.4",
fontSize: "13px"
});
loadingMsg.textContent = "Fetching source…";
editorContainer.appendChild(loadingMsg);
panel.appendChild(header);
panel.appendChild(urlBar);
panel.appendChild(editorContainer);
document.body.appendChild(panel);
enableDragOn(panel, header);
let code;
try {
const fetchUrl = normalizePluginUrl(plugin.url);
const res = await fetch(fetchUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
code = await res.text();
} catch (err) {
loadingMsg.textContent = `Failed to fetch source: ${err.message}`;
loadingMsg.style.color = "#ff4d4d";
loadingMsg.style.opacity = "1";
return;
}
editorContainer.removeChild(loadingMsg);
monaco.editor.create(editorContainer, {
value: code,
language: "javascript",
theme: "vs-dark",
readOnly: true,
automaticLayout: true,
minimap: { enabled: true },
fontSize: 13,
scrollBeyondLastLine: false,
wordWrap: "off",
domReadOnly: true,
renderValidationDecorations: "off",
renderLineHighlight: "none",
cursorStyle: "block",
cursorBlinking: "solid"
});
}
function togglePluginsPanel() {
let panel = document.getElementById('avia-plugins-panel');
if (panel) {
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
return;
}
panel = document.createElement('div');
panel.id = 'avia-plugins-panel';
Object.assign(panel.style, {
position: 'fixed',
bottom: '24px',
right: '24px',
width: '520px',
height: '460px',
background: 'var(--md-sys-color-surface, #1e1e1e)',
color: 'var(--md-sys-color-on-surface, #fff)',
borderRadius: '16px',
boxShadow: '0 8px 28px rgba(0,0,0,0.35)',
zIndex: '999999',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
border: '1px solid rgba(255,255,255,0.08)',
backdropFilter: 'blur(12px)'
});
const header = document.createElement('div');
header.textContent = 'Plugins';
Object.assign(header.style, {
padding: '14px 16px',
fontWeight: '600',
fontSize: '14px',
background: 'var(--md-sys-color-surface-container, rgba(255,255,255,0.04))',
borderBottom: '1px solid rgba(255,255,255,0.08)',
cursor: 'move'
});
const closeBtn = document.createElement('div');
closeBtn.textContent = '✕';
Object.assign(closeBtn.style, {
position: 'absolute',
top: '12px',
right: '16px',
cursor: 'pointer',
opacity: '0.7'
});
closeBtn.onclick = () => panel.style.display = 'none';
const controlsBar = document.createElement('div');
Object.assign(controlsBar.style, {
padding: '12px 16px',
display: 'flex',
gap: '8px',
alignItems: 'center',
borderBottom: '1px solid rgba(255,255,255,0.08)',
flex: '0 0 auto'
});
const content = document.createElement('div');
content.id = 'avia-plugins-content';
Object.assign(content.style, {
flex: '1',
overflow: 'auto',
padding: '16px'
});
const nameInput = document.createElement('input');
nameInput.placeholder = 'Name';
styleInput(nameInput);
nameInput.style.width = '110px';
const urlInput = document.createElement('input');
urlInput.placeholder = 'Plugin URL';
styleInput(urlInput);
urlInput.style.flex = '1';
const addBtn = document.createElement('button');
addBtn.textContent = '+ Add';
styleBtn(addBtn);
addBtn.onclick = () => {
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name || !url) return;
const plugins = getPlugins();
plugins.push({ name, url, enabled: false });
setPlugins(plugins);
nameInput.value = '';
urlInput.value = '';
renderPanel();
};
const refreshAll = document.createElement('button');
refreshAll.textContent = 'Refresh';
styleBtn(refreshAll);
refreshAll.onclick = () => {
const plugins = getPlugins();
plugins.forEach(p => {
if (p.enabled) queuePlugin(p, true);
});
};
controlsBar.appendChild(nameInput);
controlsBar.appendChild(urlInput);
controlsBar.appendChild(addBtn);
controlsBar.appendChild(refreshAll);
panel.appendChild(header);
panel.appendChild(closeBtn);
panel.appendChild(controlsBar);
panel.appendChild(content);
document.body.appendChild(panel);
enableDragOn(panel, header);
renderPanel();
}
function renderPanel() {
const content = document.getElementById('avia-plugins-content');
if (!content) return;
content.innerHTML = '';
const plugins = getPlugins();
const runningSnapshot = { ...runningPlugins };
const errorSnapshot = { ...pluginErrors };
if (plugins.length === 0) {
const empty = document.createElement('div');
empty.textContent = 'No plugins yet. Add one above.';
Object.assign(empty.style, { opacity: '0.4', fontSize: '13px' });
content.appendChild(empty);
return;
}
plugins.forEach((plugin, index) => {
const isRunning = !!runningSnapshot[plugin.url];
const hasError = !!errorSnapshot[plugin.url];
const row = document.createElement('div');
Object.assign(row.style, {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '12px',
padding: '10px 12px',
borderRadius: '10px',
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.06)'
});
const left = document.createElement('div');
Object.assign(left.style, { display: 'flex', alignItems: 'center', gap: '10px' });
const statusDot = document.createElement('div');
Object.assign(statusDot.style, {
width: '10px',
height: '10px',
borderRadius: '50%',
flexShrink: '0'
});
if (hasError) {
statusDot.style.background = '#ff4d4d';
statusDot.style.boxShadow = '0 0 6px #ff4d4d';
} else if (isRunning) {
statusDot.style.background = '#4dff88';
statusDot.style.boxShadow = '0 0 6px #4dff88';
} else {
statusDot.style.background = '#777';
}
const name = document.createElement('div');
name.textContent = plugin.name;
name.style.fontSize = '13px';
left.appendChild(statusDot);
left.appendChild(name);
const controls = document.createElement('div');
Object.assign(controls.style, { display: 'flex', gap: '6px' });
const toggle = document.createElement('button');
toggle.textContent = plugin.enabled ? 'Disable' : 'Enable';
styleBtn(toggle);
toggle.onclick = () => {
plugin.enabled = !plugin.enabled;
setPlugins(plugins);
if (plugin.enabled) queuePlugin(plugin);
else stopPlugin(plugin);
renderPanel();
};
const viewBtn = document.createElement('button');
viewBtn.textContent = 'View';
styleBtn(viewBtn, 'rgba(100,160,255,0.15)');
viewBtn.onclick = () => openViewerPanel(plugin);
const remove = document.createElement('button');
remove.textContent = '✕';
styleBtn(remove, 'rgba(255,80,80,0.15)');
remove.onclick = () => {
stopPlugin(plugin);
plugins.splice(index, 1);
setPlugins(plugins);
renderPanel();
};
controls.appendChild(toggle);
controls.appendChild(viewBtn);
controls.appendChild(remove);
row.appendChild(left);
row.appendChild(controls);
content.appendChild(row);
});
}
function styleInput(input) {
Object.assign(input.style, {
padding: '6px 8px',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.1)',
background: 'rgba(255,255,255,0.05)',
color: '#fff',
fontSize: '13px'
});
}
function styleBtn(btn, bg) {
Object.assign(btn.style, {
padding: '5px 12px',
borderRadius: '8px',
border: 'none',
background: bg || 'rgba(255,255,255,0.08)',
color: '#fff',
cursor: 'pointer',
fontSize: '12px',
whiteSpace: 'nowrap'
});
btn.onmouseenter = () => btn.style.opacity = '0.75';
btn.onmouseleave = () => btn.style.opacity = '1';
}
function enableDragOn(panel, header) {
let isDragging = false, offsetX, offsetY;
header.addEventListener('mousedown', e => {
isDragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = 'none';
});
document.addEventListener('mouseup', () => {
isDragging = false;
document.body.style.userSelect = '';
});
document.addEventListener('mousemove', e => {
if (!isDragging) return;
panel.style.left = (e.clientX - offsetX) + 'px';
panel.style.top = (e.clientY - offsetY) + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
});
}
function injectButtons() {
if (document.getElementById('stoat-fake-plugins')) return;
const appearanceBtn = [...document.querySelectorAll('a')]
.find(a => a.textContent.trim() === 'Appearance');
if (!appearanceBtn) return;
const referenceNode = document.getElementById('stoat-fake-quickcss');
if (!referenceNode) return;
const pluginsBtn = appearanceBtn.cloneNode(true);
pluginsBtn.id = 'stoat-fake-plugins';
const textNode = [...pluginsBtn.querySelectorAll('div')]
.find(d => d.children.length === 0 && d.textContent.trim() === 'Appearance');
if (textNode) textNode.textContent = "(Sanctum) Plugins";
const svgNS = "http://www.w3.org/2000/svg";
const oldSvg = pluginsBtn.querySelector('svg');
if (oldSvg) oldSvg.remove();
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "20");
svg.setAttribute("height", "20");
svg.setAttribute("fill", "currentColor");
svg.style.marginRight = "8px";
const path = document.createElementNS(svgNS, "path");
path.setAttribute("d", "M20.5 11H19V7a2 2 0 00-2-2h-4V3.5a2.5 2.5 0 00-5 0V5H4a2 2 0 00-2 2v3.8h1.5c1.5 0 2.7 1.2 2.7 2.7S5 16.2 3.5 16.2H2V20a2 2 0 002 2h3.8v-1.5c0-1.5 1.2-2.7 2.7-2.7s2.7 1.2 2.7 2.7V22H17a2 2 0 002-2v-4h1.5a2.5 2.5 0 000-5z");
svg.appendChild(path);
pluginsBtn.insertBefore(svg, pluginsBtn.firstChild);
pluginsBtn.addEventListener('click', togglePluginsPanel);
referenceNode.parentElement.insertBefore(pluginsBtn, referenceNode.nextSibling);
}
function waitForBody(callback) {
if (document.body) callback();
else new MutationObserver((obs) => {
if (document.body) { obs.disconnect(); callback(); }
}).observe(document.documentElement, { childList: true });
}
waitForBody(() => {
const observer = new MutationObserver(() => injectButtons());
observer.observe(document.body, { childList: true, subtree: true });
injectButtons();
preloadMonaco();
});
getPlugins().forEach(plugin => {
if (plugin.enabled) queuePlugin(plugin);
});
})();

479
avia_core/repofrontend.js Normal file
View file

@ -0,0 +1,479 @@
(function () {
if (window.__AVIA_OFFICIAL_REPO_LOADED__) return;
window.__AVIA_OFFICIAL_REPO_LOADED__ = true;
const STORAGE_KEY = "avia_plugins";
const OFFICIAL_REPO_URL = "https://avalilac.github.io/PluginRepo/pluginrepobackend.js";
const THEMES_REGISTRY_URL = "https://avalilac.github.io/PluginRepo/themebackend/themerepobackend.js";
const getPlugins = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
const setPlugins = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
let repoContent;
let currentRepoData = [];
let currentThemeData = [];
let searchInput;
let activeTab = "plugins"; // "plugins" | "themes"
document.getElementById("avia-official-repo-btn")?.remove();
function triggerManagerRefresh() {
const panel = document.getElementById("avia-plugins-panel");
if (!panel) return;
const refreshBtn = Array.from(panel.querySelectorAll("button"))
.find(b => b.textContent.trim() === "Refresh");
if (refreshBtn) refreshBtn.click();
}
function updateInstallStates() {
if (!repoContent) return;
const installed = getPlugins().map(p => p.url);
repoContent.querySelectorAll("[data-link]").forEach(row => {
const link = row.getAttribute("data-link");
const btn = row.querySelector("button.install-btn");
if (!btn) return;
if (installed.includes(link)) {
btn.textContent = "Installed";
btn.disabled = true;
} else {
btn.textContent = "Install";
btn.disabled = false;
}
});
}
function renderRepo(data, filter = "") {
if (!repoContent) return;
currentRepoData = data.plugins;
repoContent.innerHTML = "";
const filtered = currentRepoData.filter(p =>
(p.name + " " + (p.author || "") + " " + (p.description || ""))
.toLowerCase()
.includes(filter.toLowerCase())
);
if (filtered.length === 0) {
repoContent.innerHTML = `<div style="opacity:0.5;text-align:center;margin-top:30px;">No plugins found.</div>`;
return;
}
filtered.forEach(repoPlugin => {
const row = document.createElement("div");
row.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;width:100%;min-width:0;";
row.setAttribute("data-link", repoPlugin.link);
const left = document.createElement("div");
left.style.cssText = "display:flex;flex-direction:column;flex:1;min-width:0;";
const title = document.createElement("div");
title.textContent = `${repoPlugin.name}${repoPlugin.author || "Unknown"}`;
title.style.cssText = "font-weight:500;word-break:break-word;";
const desc = document.createElement("div");
desc.textContent = repoPlugin.description || "";
desc.style.cssText = "font-size:12px;opacity:0.7;word-break:break-word;";
left.appendChild(title);
left.appendChild(desc);
const installBtn = document.createElement("button");
installBtn.className = "install-btn";
Object.assign(installBtn.style, {
padding: "6px 10px",
borderRadius: "8px",
border: "none",
cursor: "pointer",
background: "rgba(255,255,255,0.08)",
color: "#fff",
flexShrink: "0"
});
installBtn.onclick = () => {
const plugins = getPlugins();
if (!plugins.some(p => p.url === repoPlugin.link)) {
plugins.push({ name: repoPlugin.name, url: repoPlugin.link, enabled: false });
setPlugins(plugins);
window.dispatchEvent(new Event("avia-plugin-list-changed"));
triggerManagerRefresh();
renderRepo({ plugins: currentRepoData }, searchInput.value);
}
};
row.appendChild(left);
row.appendChild(installBtn);
repoContent.appendChild(row);
});
updateInstallStates();
}
function refetchPlugins() {
if (!repoContent) return;
repoContent.innerHTML = "Loading...";
function electronFetch() {
try {
const https = require("https");
https.get(OFFICIAL_REPO_URL, res => {
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", () => renderRepo(JSON.parse(data)));
}).on("error", () => {
repoContent.innerHTML = "Failed to fetch repo.";
});
} catch {
repoContent.innerHTML = "Failed to fetch repo.";
}
}
try {
fetch(OFFICIAL_REPO_URL)
.then(res => res.json())
.then(data => renderRepo(data))
.catch(() => electronFetch());
} catch {
electronFetch();
}
}
const THEMES_STORAGE_KEY = "avia_themes";
const getStoredThemes = () => JSON.parse(localStorage.getItem(THEMES_STORAGE_KEY) || "[]");
const setStoredThemes = (data) => localStorage.setItem(THEMES_STORAGE_KEY, JSON.stringify(data));
function buildThemeCSS(theme, rawCSS) {
const header = `/* @name ${theme.name}\n @author ${theme.author || "Unknown"}\n @version 1.0\n @description Installed from Trusted Themes Repo\n*/\n`;
return header + rawCSS;
}
function installThemeCSS(theme, btn) {
btn.disabled = true;
btn.textContent = "Installing…";
fetch(theme.download)
.then(r => r.text())
.then(rawCSS => {
const css = buildThemeCSS(theme, rawCSS);
const themes = getStoredThemes();
const alreadyInstalled = themes.some(t => {
const match = t.css.match(/@name\s+(.+)/);
return match && match[1].trim() === theme.name;
});
if (alreadyInstalled) {
btn.textContent = "Installed";
return;
}
themes.push({ id: crypto.randomUUID(), css, enabled: true });
setStoredThemes(themes);
document.querySelectorAll(".avia-theme-style").forEach(e => e.remove());
getStoredThemes().forEach(t => {
if (!t.enabled) return;
const style = document.createElement("style");
style.className = "avia-theme-style";
style.textContent = t.css;
document.head.appendChild(style);
});
if (typeof window.__avia_refresh_themes_panel === "function") {
window.__avia_refresh_themes_panel();
}
btn.textContent = "Installed";
})
.catch(() => {
btn.textContent = "Install CSS";
btn.disabled = false;
alert("Failed to fetch theme CSS.");
});
}
function renderThemes(filter = "") {
if (!repoContent) return;
repoContent.innerHTML = "";
const filtered = currentThemeData.filter(t =>
(t.name + " " + (t.author || ""))
.toLowerCase()
.includes(filter.toLowerCase())
);
if (filtered.length === 0) {
repoContent.innerHTML = `<div style="opacity:0.5;text-align:center;margin-top:30px;">No themes found.</div>`;
return;
}
filtered.forEach(theme => {
const card = document.createElement("div");
card.style.cssText = "margin-bottom:14px;background:rgba(255,255,255,0.04);border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,0.07);";
if (theme.preview) {
const img = document.createElement("img");
img.src = theme.preview;
img.alt = theme.name;
img.style.cssText = "width:100%;display:block;background:#111;object-fit:contain;";
img.onerror = () => img.style.display = "none";
card.appendChild(img);
}
const info = document.createElement("div");
info.style.cssText = "display:flex;justify-content:space-between;align-items:center;padding:10px 12px;gap:8px;";
const meta = document.createElement("div");
meta.style.cssText = "display:flex;flex-direction:column;min-width:0;flex:1;";
const name = document.createElement("div");
name.textContent = theme.name;
name.style.cssText = "font-weight:500;word-break:break-word;";
const author = document.createElement("div");
author.textContent = `by ${theme.author || "Unknown"}`;
author.style.cssText = "font-size:12px;opacity:0.6;";
meta.appendChild(name);
meta.appendChild(author);
const alreadyInstalled = getStoredThemes().some(t => {
const match = t.css.match(/@name\s+(.+)/);
return match && match[1].trim() === theme.name;
});
const dlBtn = document.createElement("button");
dlBtn.textContent = alreadyInstalled ? "Installed" : "Install CSS";
dlBtn.disabled = alreadyInstalled;
Object.assign(dlBtn.style, {
padding: "6px 10px",
borderRadius: "8px",
border: "none",
cursor: alreadyInstalled ? "default" : "pointer",
background: "rgba(255,255,255,0.08)",
color: "#fff",
flexShrink: "0",
fontSize: "12px",
whiteSpace: "nowrap"
});
dlBtn.onclick = () => installThemeCSS(theme, dlBtn);
info.appendChild(meta);
info.appendChild(dlBtn);
card.appendChild(info);
repoContent.appendChild(card);
});
}
function refetchThemes() {
if (!repoContent) return;
repoContent.innerHTML = "Loading themes...";
currentThemeData = [];
fetch(THEMES_REGISTRY_URL)
.then(r => r.json())
.then(async registry => {
const sources = registry.sources || [];
const results = await Promise.allSettled(
sources.map(s => fetch(s.url).then(r => r.json()))
);
results.forEach(r => {
if (r.status === "fulfilled") {
currentThemeData.push(...(r.value.themes || []));
}
});
renderThemes(searchInput.value);
})
.catch(() => {
if (repoContent) repoContent.innerHTML = "Failed to fetch themes.";
});
}
function switchTab(tab, tabPluginsBtn, tabThemesBtn) {
activeTab = tab;
const isPlugins = tab === "plugins";
tabPluginsBtn.style.background = isPlugins ? "rgba(255,255,255,0.12)" : "transparent";
tabPluginsBtn.style.color = isPlugins ? "#fff" : "rgba(255,255,255,0.45)";
tabThemesBtn.style.background = !isPlugins ? "rgba(255,255,255,0.12)" : "transparent";
tabThemesBtn.style.color = !isPlugins ? "#fff" : "rgba(255,255,255,0.45)";
searchInput.placeholder = isPlugins
? "Search plugins, authors, or descriptions"
: "Search themes or authors";
searchInput.value = "";
if (isPlugins) {
if (currentRepoData.length > 0) renderRepo({ plugins: currentRepoData });
else refetchPlugins();
} else {
if (currentThemeData.length > 0) renderThemes();
else refetchThemes();
}
}
function openWindow() {
let panel = document.getElementById("avia-official-repo-window");
if (panel) {
panel.style.display = panel.style.display === "none" ? "flex" : "none";
return;
}
panel = document.createElement("div");
panel.id = "avia-official-repo-window";
Object.assign(panel.style, {
position: "fixed",
bottom: "40px",
right: "40px",
width: "420px",
height: "520px",
background: "#1e1e1e",
color: "#fff",
borderRadius: "20px",
boxShadow: "0 12px 35px rgba(0,0,0,0.45)",
zIndex: 999999,
display: "flex",
flexDirection: "column",
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.08)"
});
const header = document.createElement("div");
header.textContent = "Plugins & Themes Repo";
Object.assign(header.style, {
padding: "18px",
fontWeight: "600",
fontSize: "16px",
background: "rgba(255,255,255,0.04)",
borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor: "move",
position: "relative",
textAlign: "center",
userSelect: "none"
});
let isDragging = false, offsetX = 0, offsetY = 0;
header.addEventListener("mousedown", (e) => {
isDragging = true;
const rect = panel.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
panel.style.bottom = "auto";
panel.style.right = "auto";
panel.style.left = rect.left + "px";
panel.style.top = rect.top + "px";
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
panel.style.left = e.clientX - offsetX + "px";
panel.style.top = e.clientY - offsetY + "px";
});
document.addEventListener("mouseup", () => {
isDragging = false;
document.body.style.userSelect = "";
});
const close = document.createElement("div");
close.textContent = "✕";
Object.assign(close.style, { position: "absolute", right: "18px", top: "16px", cursor: "pointer" });
close.onclick = () => panel.style.display = "none";
header.appendChild(close);
const tabs = document.createElement("div");
tabs.style.cssText = "display:flex;gap:6px;padding:10px 12px 0;background:rgba(255,255,255,0.02);border-bottom:1px solid rgba(255,255,255,0.08);";
const tabStyle = "padding:6px 16px;border-radius:8px 8px 0 0;border:none;cursor:pointer;font-size:13px;font-weight:500;transition:background 0.15s,color 0.15s;font-family:inherit;";
const tabPluginsBtn = document.createElement("button");
tabPluginsBtn.textContent = "Plugins";
tabPluginsBtn.style.cssText = tabStyle;
const tabThemesBtn = document.createElement("button");
tabThemesBtn.textContent = "Themes";
tabThemesBtn.style.cssText = tabStyle;
tabPluginsBtn.onclick = () => switchTab("plugins", tabPluginsBtn, tabThemesBtn);
tabThemesBtn.onclick = () => switchTab("themes", tabPluginsBtn, tabThemesBtn);
tabs.appendChild(tabPluginsBtn);
tabs.appendChild(tabThemesBtn);
searchInput = document.createElement("input");
searchInput.placeholder = "Search plugins, authors, or descriptions";
Object.assign(searchInput.style, {
margin: "12px",
padding: "8px",
borderRadius: "8px",
border: "none",
outline: "none",
background: "rgba(255,255,255,0.06)",
color: "#fff"
});
searchInput.addEventListener("input", () => {
if (activeTab === "plugins") renderRepo({ plugins: currentRepoData }, searchInput.value);
else renderThemes(searchInput.value);
});
repoContent = document.createElement("div");
Object.assign(repoContent.style, {
flex: "1",
overflowY: "auto",
overflowX: "hidden",
padding: "0 12px 12px"
});
const container = document.createElement("div");
Object.assign(container.style, { flex: "1", display: "flex", flexDirection: "column", overflow: "hidden" });
container.appendChild(searchInput);
container.appendChild(repoContent);
panel.appendChild(header);
panel.appendChild(tabs);
panel.appendChild(container);
document.body.appendChild(panel);
switchTab("plugins", tabPluginsBtn, tabThemesBtn);
refetchPlugins();
}
function injectSettingsButton() {
if (document.getElementById("avia-official-repo-btn-settings")) return;
const appearanceBtn = [...document.querySelectorAll("a")]
.find(a => a.textContent.trim() === "Appearance");
const referenceNode = document.getElementById("stoat-fake-quickcss");
if (!appearanceBtn || !referenceNode) return;
const clone = appearanceBtn.cloneNode(true);
clone.id = "avia-official-repo-btn-settings";
const label = [...clone.querySelectorAll("div")].find(d => d.children.length === 0);
if (label) label.textContent = "(Sanctum) Plugins/Themes Repo";
const iconSpan = clone.querySelector("span.material-symbols-outlined");
if (iconSpan) {
iconSpan.textContent = "extension";
iconSpan.style.fontVariationSettings = "'FILL' 0,'wght' 400,'GRAD' 0";
}
clone.onclick = openWindow;
referenceNode.parentElement.insertBefore(clone, referenceNode.nextSibling);
}
window.addEventListener("avia-plugin-list-changed", () => {
if (document.getElementById("avia-official-repo-window")) {
updateInstallStates();
}
});
new MutationObserver(() => injectSettingsButton())
.observe(document.body, { childList: true, subtree: true });
injectSettingsButton();
})();

522
avia_core/themes.js Normal file
View file

@ -0,0 +1,522 @@
(function () {
if (window.__AVIA_THEMES_LOADED__) return;
window.__AVIA_THEMES_LOADED__ = true;
const STORAGE_KEY = "avia_themes";
let editingThemeId = null;
let monacoEditorInstance = null;
const TEMPLATE = `/*
@name Whatever name here
@author Whatever Author Here
@version 1.0
@description Whatever description here
*/
`;
const getThemes = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
const setThemes = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
function preloadMonaco() {
return new Promise(resolve => {
if (window.monaco) return resolve();
const loader = document.createElement("script");
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
loader.onload = function () {
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
require(["vs/editor/editor.main"], () => resolve());
};
document.head.appendChild(loader);
});
}
function parseMeta(css) {
const name = css.match(/@name\s+(.+)/)?.[1] || "Unknown Theme";
const author = css.match(/@author\s+(.+)/)?.[1] || "Unknown";
const version = css.match(/@version\s+(.+)/)?.[1] || "1.0";
const rawDescription = css.match(/@description\s+(.+)/)?.[1] || "No Description Available";
const description = rawDescription.trim() === "*/" ? "No Description Available" : rawDescription;
return { name, author, version, description };
}
function sanitizeFilename(name) {
return name
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "")
.replace(/\s+/g, "_")
.replace(/\.+$/, "")
.trim() || "theme";
}
function downloadTheme(theme) {
const name = parseMeta(theme.css).name;
const filename = sanitizeFilename(name) + ".css";
const blob = new Blob([theme.css], { type: "text/css" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function applyThemes() {
document.querySelectorAll(".avia-theme-style").forEach(e => e.remove());
getThemes().forEach(theme => {
if (!theme.enabled) return;
const style = document.createElement("style");
style.className = "avia-theme-style";
style.textContent = theme.css;
document.head.appendChild(style);
});
}
function styleBtn(btn, bg) {
Object.assign(btn.style, {
padding: "5px 12px",
borderRadius: "8px",
border: "none",
background: bg || "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
fontSize: "12px",
whiteSpace: "nowrap",
fontWeight: "500"
});
btn.onmouseenter = () => btn.style.opacity = "0.75";
btn.onmouseleave = () => btn.style.opacity = "1";
}
function makeDraggable(panel, handle) {
let dragging = false, offsetX, offsetY;
handle.addEventListener("mousedown", e => {
dragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = "none";
});
document.addEventListener("mouseup", () => { dragging = false; document.body.style.userSelect = ""; });
document.addEventListener("mousemove", e => {
if (!dragging) return;
panel.style.left = (e.clientX - offsetX) + "px";
panel.style.top = (e.clientY - offsetY) + "px";
panel.style.right = "auto";
panel.style.bottom = "auto";
});
}
async function openThemeEditor(themeId) {
await preloadMonaco();
editingThemeId = themeId;
const themes = getThemes();
const theme = themes.find(t => t.id === themeId);
if (!theme) return;
const meta = parseMeta(theme.css);
let panel = document.getElementById("avia-theme-editor");
if (panel) {
panel.style.display = "flex";
panel.querySelector("#avia-theme-editor-title").textContent = "Theme Editor — " + meta.name;
if (monacoEditorInstance) {
monacoEditorInstance._aviaThemeId = themeId;
const model = monacoEditorInstance.getModel();
if (model) model.setValue(theme.css || "");
}
return;
}
panel = document.createElement("div");
panel.id = "avia-theme-editor";
Object.assign(panel.style, {
position: "fixed",
bottom: "24px",
right: "24px",
width: "650px",
height: "420px",
background: "var(--md-sys-color-surface, #1e1e1e)",
color: "var(--md-sys-color-on-surface, #fff)",
borderRadius: "16px",
boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
zIndex: "9999999",
display: "flex",
flexDirection: "column",
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.08)",
backdropFilter: "blur(12px)"
});
const header = document.createElement("div");
header.id = "avia-theme-editor-title";
header.textContent = "Theme Editor — " + meta.name;
Object.assign(header.style, {
padding: "14px 16px",
fontWeight: "600",
fontSize: "14px",
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor: "move",
color: "#fff",
flex: "0 0 auto"
});
makeDraggable(panel, header);
const close = document.createElement("div");
close.textContent = "✕";
Object.assign(close.style, {
position: "absolute",
right: "16px",
top: "12px",
cursor: "pointer",
opacity: "0.6",
fontSize: "15px",
lineHeight: "1",
padding: "2px 4px",
color: "#fff"
});
close.onmouseenter = () => close.style.opacity = "1";
close.onmouseleave = () => close.style.opacity = "0.6";
close.onclick = () => panel.style.display = "none";
const editorContainer = document.createElement("div");
editorContainer.style.flex = "1";
panel.appendChild(header);
panel.appendChild(close);
panel.appendChild(editorContainer);
document.body.appendChild(panel);
monacoEditorInstance = monaco.editor.create(editorContainer, {
value: theme.css || "",
language: "css",
theme: "vs-dark",
automaticLayout: true,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
wordWrap: "on"
});
monacoEditorInstance._aviaThemeId = themeId;
monacoEditorInstance.onDidChangeModelContent(() => {
const id = monacoEditorInstance._aviaThemeId;
if (!id) return;
const value = monacoEditorInstance.getValue();
const all = getThemes();
const target = all.find(t => t.id === id);
if (!target) return;
target.css = value;
setThemes(all);
applyThemes();
header.textContent = "Theme Editor — " + parseMeta(value).name;
if (typeof window.__avia_refresh_themes_panel === "function") {
window.__avia_refresh_themes_panel();
}
});
}
function toggleThemesPanel() {
let panel = document.getElementById("avia-themes-panel");
if (panel) {
panel.style.display = panel.style.display === "none" ? "flex" : "none";
return;
}
panel = document.createElement("div");
panel.id = "avia-themes-panel";
Object.assign(panel.style, {
position: "fixed",
bottom: "40px",
right: "40px",
width: "500px",
height: "460px",
background: "var(--md-sys-color-surface, #1e1e1e)",
color: "var(--md-sys-color-on-surface, #fff)",
borderRadius: "16px",
boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
zIndex: "999999",
display: "flex",
flexDirection: "column",
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.08)",
backdropFilter: "blur(12px)"
});
const header = document.createElement("div");
header.textContent = "Themes";
Object.assign(header.style, {
padding: "14px 16px",
fontWeight: "600",
fontSize: "14px",
background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor: "move"
});
makeDraggable(panel, header);
const close = document.createElement("div");
close.textContent = "✕";
Object.assign(close.style, {
position: "absolute",
right: "16px",
top: "12px",
cursor: "pointer",
opacity: "0.6",
fontSize: "15px",
lineHeight: "1",
padding: "2px 4px"
});
close.onmouseenter = () => close.style.opacity = "1";
close.onmouseleave = () => close.style.opacity = "0.6";
close.onclick = () => panel.style.display = "none";
const btnRow = document.createElement("div");
Object.assign(btnRow.style, {
display: "flex",
gap: "8px",
padding: "12px 16px",
borderBottom: "1px solid rgba(255,255,255,0.08)",
flex: "0 0 auto"
});
const importBtn = document.createElement("button");
importBtn.textContent = "Import Theme";
styleBtn(importBtn);
importBtn.style.flex = "1";
importBtn.style.padding = "8px 12px";
const newBtn = document.createElement("button");
newBtn.textContent = "+ New";
styleBtn(newBtn);
newBtn.style.flex = "1";
newBtn.style.padding = "8px 12px";
btnRow.appendChild(importBtn);
btnRow.appendChild(newBtn);
const list = document.createElement("div");
Object.assign(list.style, {
flex: "1",
overflowY: "auto",
padding: "16px",
display: "flex",
flexDirection: "column",
gap: "8px"
});
const dropOverlay = document.createElement("div");
dropOverlay.textContent = "Drop .css or .txt files here";
Object.assign(dropOverlay.style, {
position: "absolute",
inset: "0",
background: "rgba(0,0,0,0.6)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "18px",
fontWeight: "600",
color: "#fff",
opacity: "0",
pointerEvents: "none",
transition: "opacity 0.15s ease",
borderRadius: "16px"
});
panel.appendChild(header);
panel.appendChild(close);
panel.appendChild(btnRow);
panel.appendChild(list);
panel.appendChild(dropOverlay);
document.body.appendChild(panel);
let dragDepth = 0;
panel.addEventListener("dragenter", e => {
e.preventDefault();
e.stopPropagation();
dragDepth++;
dropOverlay.style.opacity = "1";
panel.style.border = "1px dashed rgba(255,255,255,0.4)";
});
panel.addEventListener("dragover", e => {
e.preventDefault();
e.stopPropagation();
});
panel.addEventListener("dragleave", e => {
e.preventDefault();
e.stopPropagation();
dragDepth--;
if (dragDepth <= 0) {
dropOverlay.style.opacity = "0";
panel.style.border = "1px solid rgba(255,255,255,0.08)";
dragDepth = 0;
}
});
panel.addEventListener("drop", async e => {
e.preventDefault();
e.stopPropagation();
dropOverlay.style.opacity = "0";
panel.style.border = "1px solid rgba(255,255,255,0.08)";
dragDepth = 0;
const files = [...e.dataTransfer.files].filter(f => f.name.endsWith(".css") || f.name.endsWith(".txt"));
if (!files.length) return;
const themes = getThemes();
for (const file of files) {
const css = await file.text();
themes.push({ id: crypto.randomUUID(), css, enabled: true });
}
setThemes(themes);
applyThemes();
render();
});
function render() {
list.innerHTML = "";
const themes = getThemes();
if (themes.length === 0) {
const empty = document.createElement("div");
empty.textContent = "No themes yet. Import or create one above.";
Object.assign(empty.style, { opacity: "0.4", fontSize: "13px" });
list.appendChild(empty);
return;
}
themes.forEach(theme => {
const meta = parseMeta(theme.css);
const card = document.createElement("div");
Object.assign(card.style, {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 12px",
borderRadius: "10px",
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.06)"
});
const left = document.createElement("div");
Object.assign(left.style, { display: "flex", alignItems: "center", gap: "10px" });
const dot = document.createElement("div");
Object.assign(dot.style, {
width: "10px",
height: "10px",
borderRadius: "50%",
flexShrink: "0",
background: theme.enabled ? "#4dff88" : "#777",
boxShadow: theme.enabled ? "0 0 6px #4dff88" : "none"
});
const info = document.createElement("div");
info.innerHTML = `<div style="font-weight:600;font-size:13px">${meta.name}</div><div style="font-size:11px;opacity:.5">${meta.author} • v${meta.version}</div><div style="font-size:11px;opacity:.4">${meta.description}</div>`;
left.appendChild(dot);
left.appendChild(info);
const controls = document.createElement("div");
Object.assign(controls.style, { display: "flex", gap: "6px" });
const toggle = document.createElement("button");
toggle.textContent = theme.enabled ? "Disable" : "Enable";
styleBtn(toggle);
toggle.onclick = () => {
theme.enabled = !theme.enabled;
setThemes(themes);
applyThemes();
render();
};
const edit = document.createElement("button");
edit.textContent = "Edit";
styleBtn(edit, "rgba(100,160,255,0.15)");
edit.onclick = () => openThemeEditor(theme.id);
const dlBtn = document.createElement("button");
dlBtn.textContent = "Export";
styleBtn(dlBtn, "rgba(80,200,120,0.15)");
dlBtn.title = "Download theme as .css";
dlBtn.onclick = e => {
e.stopPropagation();
downloadTheme(theme);
};
const del = document.createElement("button");
del.textContent = "✕";
styleBtn(del, "rgba(255,80,80,0.15)");
del.onclick = () => {
const updated = themes.filter(t => t.id !== theme.id);
setThemes(updated);
applyThemes();
render();
};
controls.appendChild(toggle);
controls.appendChild(edit);
controls.appendChild(dlBtn);
controls.appendChild(del);
card.appendChild(left);
card.appendChild(controls);
list.appendChild(card);
});
}
window.__avia_refresh_themes_panel = render;
importBtn.onclick = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".css,.txt";
input.multiple = true;
input.onchange = async () => {
const files = [...input.files];
if (!files.length) return;
const themes = getThemes();
for (const file of files) {
const css = await file.text();
themes.push({ id: crypto.randomUUID(), css, enabled: true });
}
setThemes(themes);
applyThemes();
render();
};
input.click();
};
newBtn.onclick = () => {
const themes = getThemes();
themes.push({ id: crypto.randomUUID(), css: TEMPLATE, enabled: true });
setThemes(themes);
applyThemes();
render();
};
render();
}
function injectButton() {
if (document.getElementById("avia-themes-btn")) return;
const appearanceBtn = [...document.querySelectorAll("a")].find(a => a.textContent.trim() === "Appearance");
const quickCSS = document.getElementById("stoat-fake-quickcss");
if (!appearanceBtn || !quickCSS) return;
const clone = appearanceBtn.cloneNode(true);
clone.id = "avia-themes-btn";
const text = [...clone.querySelectorAll("div")].find(d => d.children.length === 0);
if (text) text.textContent = "(Sanctum) Themes";
clone.onclick = toggleThemesPanel;
quickCSS.parentElement.insertBefore(clone, quickCSS.nextSibling);
}
new MutationObserver(injectButton).observe(document.body, { childList: true, subtree: true });
injectButton();
applyThemes();
preloadMonaco();
})();

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Name=Sanctum
Comment=Open source, user-first chat platform
Exec=sanctum
Terminal=false
Type=Application
Icon=cloud.mithraic.sanctum
Categories=Network;InstantMessaging
StartupWMClass=sanctum
X-Desktop-File-Install-Version=0.26

View file

@ -0,0 +1,36 @@
<?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-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>

View file

@ -6,32 +6,27 @@ 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 { PublisherGithub } from "@electron-forge/publisher-github";
import { VitePluginBuildConfig } from "@electron-forge/plugin-vite/dist/Config";
import type { ForgeConfig } from "@electron-forge/shared-types";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
// import { globSync } from "node:fs";
import * as fs from "fs";
const STRINGS = {
author: "Revolt Platforms LTD",
name: "Stoat",
execName: "stoat-desktop",
author: "izzy",
name: "Sanctum",
execName: "sanctum",
description: "Open source user-first chat platform.",
};
const ASSET_DIR = "assets/desktop";
const AVIA_ASSET_DIR = "avia_assets";
/**
* Build targets for the desktop app
*/
const makers: ForgeConfig["makers"] = [
new MakerSquirrel({
name: STRINGS.name,
authors: STRINGS.author,
// todo: hoist this
iconUrl: `https://stoat.chat/app/assets/icon-DUSNE-Pb.ico`,
// todo: loadingGif
setupIcon: `${ASSET_DIR}/icon.ico`,
setupIcon: `${AVIA_ASSET_DIR}/icon.ico`,
description: STRINGS.description,
exe: `${STRINGS.execName}.exe`,
setupExe: `${STRINGS.execName}-setup.exe`,
@ -40,21 +35,16 @@ const makers: ForgeConfig["makers"] = [
new MakerZIP({}),
];
// skip these makers in CI/CD
if (!process.env.PLATFORM) {
makers.push(
// must be manually built (freezes CI process)
// not much use in being published anyhow
new MakerAppX({
certPass: "",
packageExecutable: `app\\${STRINGS.execName}.exe`,
publisher: "CN=B040CC7E-0016-4AF5-957F-F8977A6CFA3B",
}),
// flatpak publishing should occur through flathub repos.
// this is just for testing purposes
new MakerFlatpak({
options: {
id: "chat.stoat.stoat-desktop",
id: "cloud.mithraic.sanctum",
description: STRINGS.description,
productName: STRINGS.name,
productDescription: STRINGS.description,
@ -69,7 +59,6 @@ if (!process.env.PLATFORM) {
} as unknown,
categories: ["Network"],
modules: [
// use the latest zypak -- Electron sandboxing for Flatpak
{
name: "zypak",
sources: [
@ -82,8 +71,6 @@ if (!process.env.PLATFORM) {
},
],
finishArgs: [
// default arguments found by running
// DEBUG=electron-installer-flatpak* pnpm make
"--socket=fallback-x11",
"--share=ipc",
"--device=dri",
@ -92,77 +79,73 @@ if (!process.env.PLATFORM) {
"--env=TMPDIR=/var/tmp",
"--share=network",
"--talk-name=org.freedesktop.Notifications",
// add Unity talk name for badges
"--talk-name=com.canonical.Unity",
],
// files: [
// // is this necessary?
// // https://stackoverflow.com/q/79745700
// ...[16, 32, 64, 128, 256, 512].map(
// (size) =>
// [
// `assets/desktop/hicolor/${size}x${size}.png`,
// `/app/share/icons/hicolor/${size}x${size}/apps/chat.stoat.stoat-desktop.png`,
// ] as [string, string],
// ),
// [
// `assets/desktop/icon.svg`,
// `/app/share/icons/hicolor/scalable/apps/chat.stoat.stoat-desktop.svg`,
// ] as [string, string],
// ],
files: [],
} as MakerFlatpakOptionsConfig,
/* as Omit<
MakerFlatpakOptionsConfig,
"files"
> */
}),
// testing purposes
new MakerDeb({
options: {
productName: STRINGS.name,
productDescription: STRINGS.description,
categories: ["Network"],
icon: `${ASSET_DIR}/icon.png`,
icon: `${AVIA_ASSET_DIR}/icon.png`,
},
}),
);
}
const customVitePluginBuild: VitePluginBuildConfig[] = [
{
entry: "avia_assets/icon.png",
config: "vite.main.config.ts",
target: "main",
},
{
entry: "about.html",
config: "vite.main.config.ts",
target: "main",
},
{
entry: "src/main.ts",
config: "vite.main.config.ts",
target: "main",
},
{
entry: "src/preload.ts",
config: "vite.preload.config.ts",
target: "preload",
},
];
fs.readdir("avia_core", (err: NodeJS.ErrnoException, files: string[]) => {
if (err) return;
for (const file of files) {
if (["js", "ts", "tsx"].includes(file.split(".").pop().toLowerCase())) {
customVitePluginBuild.push({
entry: `avia_core/${file}`,
config: "vite.main.config.ts",
target: "main",
});
}
}
});
const config: ForgeConfig = {
packagerConfig: {
asar: true,
name: STRINGS.name,
executableName: STRINGS.execName,
icon: `${ASSET_DIR}/icon`,
// extraResource: [
// // include all the asset files
// ...globSync(ASSET_DIR + "/**/*"),
// ],
icon: `${AVIA_ASSET_DIR}/icon`,
},
rebuildConfig: {},
makers,
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: "src/main.ts",
config: "vite.main.config.ts",
target: "main",
},
{
entry: "src/preload.ts",
config: "vite.preload.config.ts",
target: "preload",
},
],
build: customVitePluginBuild,
renderer: [],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
@ -173,14 +156,6 @@ const config: ForgeConfig = {
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
publishers: [
new PublisherGithub({
repository: {
owner: "stoatchat",
name: "for-desktop",
},
}),
],
};
export default config;

View file

@ -1,9 +1,10 @@
{
"name": "stoat-desktop",
"productName": "stoat-desktop",
"version": "1.3.0",
"name": "sanctum",
"productName": "Sanctum",
"version": "1.0.0",
"aviaVersion": "1.0.0",
"main": ".vite/build/main.js",
"repository": "stoatchat/desktop",
"repository": "https://git.mithraic.cloud/ad3laid3/sanctum",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
@ -37,6 +38,7 @@
"@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",
@ -52,8 +54,7 @@
"discord-rpc": "^4.0.1",
"electron-squirrel-startup": "^1.0.1",
"electron-store": "^10.1.0",
"update-electron-app": "^3.1.1",
"utf-8-validate": "^6.0.5"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}
"packageManager": "pnpm@10.33.0"
}

598
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 467 KiB

2
src/config.d.ts vendored
View file

@ -1,7 +1,9 @@
declare type DesktopConfig = {
firstLaunch: boolean;
customFrame: boolean;
customFrameNativeMenu: boolean;
minimiseToTray: boolean;
disableTrayClick: boolean;
spellchecker: boolean;
hardwareAcceleration: boolean;
discordRpc: boolean;

4
src/hackfix.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*?asset" {
export const assetURL: string;
export default assetURL;
}

View file

@ -1,49 +1,94 @@
import { IUpdateInfo, updateElectronApp } from "update-electron-app";
import * as fs from "fs";
import * as path from "path";
import { BrowserWindow, Notification, app, shell } from "electron";
import { BrowserWindow, app, shell } from "electron";
import started from "electron-squirrel-startup";
import { 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 { checkForUpdates } from "./native/updater";
import { initTray } from "./native/tray";
import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
// Squirrel-specific logic
// create/remove shortcuts on Windows when installing / uninstalling
// we just need to close out of the app immediately
if (process.platform === "linux") {
app.commandLine.appendSwitch("disable-gpu-sandbox");
app.commandLine.appendSwitch("no-zygote");
app.commandLine.appendSwitch("use-gl", "desktop");
}
const applyAppName = () => {
try {
app.setName("Sanctum");
app.name = "Sanctum";
if (process.platform === "win32") {
app.setAppUserModelId("cloud.mithraic.sanctum");
}
} catch {
/* empty */
}
};
if (started) {
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 onNotifyUser = (_info: IUpdateInfo) => {
const notification = new Notification({
title: "Update Available",
body: "Restart the app to install the update.",
silent: true,
});
const loadInject = () => {
if (!mainWindow) return;
notification.show();
mainWindow.webContents.on("dom-ready", async () => {
try {
const plugins: string[] = [
"inject.js",
"LocalPlugins.js",
"aviaclientcategory.js",
"themes.js",
"aviafavsystem.js",
"pluginsupport.js",
"aviaversion.js",
"repofrontend.js",
"ButtonFix.js",
"headliner.js",
"aviadesktopversion.js",
"customFrameNativeMenu.js",
"disableTrayIcon.js",
"clientBackup.js",
"LoginWithToken.js",
];
for (const plugin of plugins) {
const pluginPath: string = path.join(__dirname, plugin);
const pluginCode: string = fs.readFileSync(pluginPath, "utf8");
await mainWindow.webContents.executeJavaScript(pluginCode, true);
}
} catch {
/* empty */
}
});
};
if (acquiredLock) {
// start auto update logic
updateElectronApp({ onNotifyUser });
// create and configure the app when electron is ready
app.on("ready", () => {
// create window and application contexts
app.whenReady().then(() => {
applyAppName();
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();
@ -53,23 +98,26 @@ if (acquiredLock) {
initTray();
initDiscordRpc();
checkForUpdates();
setBadgeCount(0);
// Windows specific fix for notifications
if (process.platform === "win32") {
app.setAppUserModelId("chat.stoat.notifications");
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();
@ -79,22 +127,27 @@ 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:") ||

61
src/native/about.ts Normal file
View file

@ -0,0 +1,61 @@
import { join } from "node:path";
import { BrowserWindow } from "electron";
import { mainWindow } from "./window";
// global reference to about window
export let aboutWindow: BrowserWindow;
// Create our about window
export function createAboutWindow() {
// If our about window already exists, show it
if (aboutWindow) {
aboutWindow.show();
return;
}
aboutWindow = new BrowserWindow({
minWidth: 300,
minHeight: 300,
width: 1024,
height: 720,
center: true,
backgroundColor: "#191919",
frame: true,
resizable: false,
minimizable: false,
parent: mainWindow,
paintWhenInitiallyHidden: true,
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
spellcheck: false,
devTools: false,
},
});
// Remove the default menu
aboutWindow.setMenu(null);
aboutWindow.loadFile(join(__dirname, "about.html"));
aboutWindow.on("ready-to-show", () => {
aboutWindow.show();
});
aboutWindow.on("closed", () => {
mainWindow.show();
mainWindow.focus();
aboutWindow = null;
});
// Close window on Escape
aboutWindow.webContents.on("before-input-event", (event, input) => {
if (input.key.toLowerCase() === "escape") {
event.preventDefault();
aboutWindow.close();
}
});
}

View file

@ -2,8 +2,6 @@ import AutoLaunch from "auto-launch";
import { ipcMain } from "electron";
import { mainWindow } from "./window";
export const autoLaunch = new AutoLaunch({
name: "Stoat",
});

View file

@ -13,9 +13,15 @@ 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,
@ -55,7 +61,9 @@ const store = new Store({
defaults: {
firstLaunch: true,
customFrame: true,
customFrameNativeMenu: false,
minimiseToTray: true,
disableTrayClick: false,
startMinimisedToTray: false,
spellchecker: true,
hardwareAcceleration: true,
@ -78,7 +86,9 @@ 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,
@ -113,6 +123,34 @@ 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",

View file

@ -16,8 +16,8 @@ export async function initDiscordRpc() {
rpc.on("ready", () =>
rpc.setActivity({
details: "Chatting with others on Sanctum",
state: "stoat.chat",
details: "Chatting with others",
largeImageKey: "qr",
largeImageText: "Join Stoat!",
buttons: [
@ -31,7 +31,7 @@ export async function initDiscordRpc() {
rpc.on("disconnected", reconnect);
rpc.login({ clientId: "872068124005007420" });
rpc.login({ clientId: "1490783938829090837" });
} catch (err) {
reconnect();
}

View file

@ -1,9 +1,11 @@
import { Menu, Tray, nativeImage } from "electron";
import { Menu, Tray, app, nativeImage } from "electron";
import trayIconAsset from "../../assets/desktop/icon.png?asset";
import macOsTrayIconAsset from "../../assets/desktop/iconTemplate.png?asset";
import { version } from "../../package.json";
import trayIconAsset from "../../avia_assets/icon.png?asset";
import macOsTrayIconAsset from "../../avia_assets/iconTemplate.png?asset";
import { aviaVersion, version } from "../../package.json";
import { createAboutWindow } from "./about";
import { config } from "./config";
import { mainWindow, quitApp } from "./window";
// internal tray state
@ -25,14 +27,18 @@ export function initTray() {
const trayIcon = createTrayIcon();
tray = new Tray(trayIcon);
updateTrayMenu();
tray.setToolTip("Stoat for Desktop");
tray.setToolTip("Sanctum for Desktop");
tray.setImage(trayIcon);
tray.on("click", () => {
config.sync();
if (config.disableTrayClick) {
return;
}
if (mainWindow.isVisible()) {
mainWindow.hide();
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
mainWindow.show();
mainWindow.focus();
}
});
}
@ -40,18 +46,30 @@ export function initTray() {
export function updateTrayMenu() {
tray.setContextMenu(
Menu.buildFromTemplate([
{ label: "Stoat for Desktop", type: "normal", enabled: false },
{ label: "Sanctum for Desktop", type: "normal", enabled: false },
{
label: "Version",
label: "Versions",
type: "submenu",
submenu: Menu.buildFromTemplate([
{
label: version,
label: `Stoat Desktop: ${version}`,
type: "normal",
enabled: false,
},
{
label: `Sanctum: ${aviaVersion}`,
type: "normal",
enabled: false,
},
]),
},
{
label: "About",
type: "normal",
click() {
createAboutWindow();
},
},
{ type: "separator" },
{
label: mainWindow.isVisible() ? "Hide App" : "Show App",
@ -64,6 +82,15 @@ export function updateTrayMenu() {
}
},
},
{ type: "separator" },
{
label: "Restart App",
type: "normal",
click() {
app.relaunch();
app.quit();
},
},
{
label: "Quit App",
type: "normal",

View file

@ -0,0 +1,83 @@
import { BrowserWindow, app, ipcMain } from "electron";
let win: BrowserWindow | null = null;
const HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,sans-serif;background:#1e1e2e;color:#cdd6f4;
display:flex;flex-direction:column;align-items:center;justify-content:center;
height:100vh;padding:28px 32px;gap:14px;user-select:none;-webkit-app-region:drag}
h2{font-size:15px;font-weight:600;letter-spacing:.3px}
#status{font-size:13px;color:#a6adc8}
.track{width:100%;height:6px;background:#313244;border-radius:3px}
#bar{height:6px;background:#89b4fa;border-radius:3px;width:0%;transition:width .4s ease}
#btn{display:none;margin-top:6px;padding:9px 24px;background:#89b4fa;color:#1e1e2e;
border:none;border-radius:6px;font-size:13px;font-weight:700;cursor:pointer;
-webkit-app-region:no-drag}
#btn:hover{background:#b4befe}
</style></head><body>
<h2 id="title">Updating Sanctum</h2>
<div id="status">Downloading</div>
<div class="track"><div id="bar"></div></div>
<button id="btn">Restart Now</button>
<script>
const {ipcRenderer}=require('electron');
ipcRenderer.on('upd-progress',(_,p)=>{
document.getElementById('bar').style.width=p+'%';
document.getElementById('status').textContent=p<100?'Downloading… '+p+'%':'Installing…';
});
ipcRenderer.on('upd-ready',(_,v)=>{
document.getElementById('title').textContent='Sanctum '+v+' installed';
document.getElementById('bar').style.width='100%';
var s=document.getElementById('status');
var n=2;
s.textContent='Restarting in '+n+'…';
var t=setInterval(function(){
n--;
if(n<=0){clearInterval(t);s.textContent='Restarting…';}
else s.textContent='Restarting in '+n+'…';
},1000);
});
ipcRenderer.on('upd-error',(_,msg)=>{
document.getElementById('title').textContent='Update Failed';
document.getElementById('status').textContent=msg;
document.getElementById('bar').style.background='#f38ba8';
});
</script></body></html>`;
export function showUpdateWindow() {
if (win) { win.focus(); return; }
win = new BrowserWindow({
width: 400,
height: 180,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
title: "Sanctum Update",
frame: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(HTML));
win.on("closed", () => { win = null; });
}
export function setUpdateProgress(percent: number) {
win?.webContents.send("upd-progress", Math.round(percent));
}
export function setUpdateReady(version: string, relaunch: boolean) {
win?.webContents.send("upd-ready", version);
setTimeout(() => {
if (relaunch) app.relaunch();
app.exit(0);
}, 3000);
}
export function setUpdateError(msg: string) {
win?.webContents.send("upd-error", msg);
}

154
src/native/updater.ts Normal file
View file

@ -0,0 +1,154 @@
import { Notification, app, ipcMain } from "electron";
import { exec, spawn } from "child_process";
import { createWriteStream, mkdirSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { tmpdir } from "os";
import { showUpdateWindow, setUpdateProgress, setUpdateReady, setUpdateError } from "./update-window";
ipcMain.handle("checkForUpdates", () => checkForUpdates());
const RELEASES_URL =
"https://git.mithraic.cloud/api/v1/repos/ad3laid3/sanctum/releases/latest";
interface Asset {
name: string;
browser_download_url: string;
}
interface Release {
tag_name: string;
html_url: string;
assets: Asset[];
}
export async function checkForUpdates() {
try {
console.log("[updater] checking:", RELEASES_URL);
const res = await fetch(RELEASES_URL);
if (!res.ok) {
console.error("[updater] releases API returned", res.status, res.statusText);
notify("Update Check Failed", `API returned ${res.status}`);
return;
}
const release = (await res.json()) as Release;
const latest = release.tag_name.replace(/^v/, "");
const current = app.getVersion();
console.log(`[updater] current=${current} latest=${latest}`);
if (!isNewer(latest, current)) {
notify("Already Up to Date", `You're on Sanctum ${current}.`);
return;
}
const asset = findAsset(release.assets);
if (!asset) {
const names = release.assets.map(a => a.name).join(", ");
console.error("[updater] no matching asset for platform:", process.platform, names);
notify("Update Failed", `No ${process.platform} asset found.`);
return;
}
console.log(`[updater] update available: ${current}${latest}, downloading ${asset.name}`);
showUpdateWindow();
await downloadAndInstall(asset.browser_download_url, latest);
} catch (err) {
console.error("[updater] update check failed:", err);
setUpdateError(String(err));
}
}
function findAsset(assets: Asset[]): Asset | undefined {
if (process.platform === "linux")
return assets.find((a) => a.name.includes("linux") && a.name.endsWith(".zip"));
if (process.platform === "win32")
return assets.find((a) => a.name.includes("win32") && a.name.endsWith(".zip"));
}
async function downloadAndInstall(url: string, version: string) {
const tmpDir = join(tmpdir(), `sanctum-update-${version}`);
mkdirSync(tmpDir, { recursive: true });
const zipPath = join(tmpDir, "update.zip");
const extractDir = join(tmpDir, "extracted");
const installDir = dirname(process.execPath);
console.log(`[updater] downloading from ${url}`);
const res = await fetch(url);
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
const total = Number(res.headers.get("content-length")) || 0;
let downloaded = 0;
const writer = createWriteStream(zipPath);
const reader = res.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
writer.write(value);
downloaded += value.length;
if (total > 0) setUpdateProgress(Math.round((downloaded / total) * 90));
}
await new Promise<void>(resolve => writer.end(resolve));
setUpdateProgress(95);
console.log(`[updater] download complete, extracting to ${installDir}`);
if (process.platform === "win32") {
// Extract zip while the app is still running (no locked files yet)
await new Promise<void>((resolve, reject) => {
const cmd = `powershell -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'"`;
exec(cmd, (err, _stdout, stderr) => {
if (err) { console.error("[updater] extract failed:", stderr); reject(err); }
else resolve();
});
});
// Write a batch script that runs after we exit: waits, copies, relaunches
const batchPath = join(tmpDir, "apply-update.bat");
const bat = [
"@echo off",
"timeout /t 3 /nobreak >nul",
`for /d %%D in ("${extractDir}\\*") do set SUB=%%D`,
`xcopy /E /Y /I "%SUB%\\*" "${installDir}\\"`,
`start "" "${join(installDir, "sanctum.exe")}"`,
`del "%~f0"`,
].join("\r\n");
writeFileSync(batchPath, bat);
// Spawn batch truly detached so it outlives this process
const child = spawn("cmd.exe", ["/C", batchPath], {
detached: true,
stdio: "ignore",
windowsHide: false,
});
child.unref();
setUpdateReady(version, false); // batch script handles relaunch
} else {
await new Promise<void>((resolve, reject) => {
const cmd = `unzip -o "${zipPath}" -d "${extractDir}" && SUBDIR=$(ls "${extractDir}" | head -1) && rm -f "${installDir}/sanctum" && cp -rT "${extractDir}/$SUBDIR" "${installDir}"`;
exec(cmd, { shell: "/bin/bash" }, (err, _stdout, stderr) => {
if (err) { console.error("[updater] extract failed:", stderr); reject(err); }
else resolve();
});
});
setUpdateReady(version, true);
}
}
function notify(title: string, body: string) {
const n = new Notification({ title, body, silent: true });
n.show();
return n;
}
function isNewer(latest: string, current: string): boolean {
const parse = (v: string) => v.split(".").map(p => Number(p) || 0);
const [lA, lB, lC] = parse(latest);
const [cA, cB, cC] = parse(current);
if (lA !== cA) return lA > cA;
if (lB !== cB) return lB > cB;
return lC > cC;
}

View file

@ -9,8 +9,9 @@ import {
nativeImage,
} from "electron";
import windowIconAsset from "../../assets/desktop/icon.png?asset";
import windowIconAsset from "../../avia_assets/icon.png?asset";
import { aboutWindow, createAboutWindow } from "./about";
import { config } from "./config";
import { updateTrayMenu } from "./tray";
@ -21,7 +22,7 @@ export let mainWindow: BrowserWindow;
export const BUILD_URL = new URL(
app.commandLine.hasSwitch("force-server")
? app.commandLine.getSwitchValue("force-server")
: /*MAIN_WINDOW_VITE_DEV_SERVER_URL ??*/ "https://stoat.chat/app",
: /*MAIN_WINDOW_VITE_DEV_SERVER_URL ??*/ "https://mithraic.space/app",
);
// internal window state
@ -48,6 +49,18 @@ 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: {
@ -56,6 +69,7 @@ export function createMainWindow() {
contextIsolation: true,
nodeIntegration: false,
spellcheck: true,
devTools: true,
},
});
@ -132,17 +146,60 @@ 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();
})();`);
}
});
// send the config
mainWindow.webContents.on("did-finish-load", () => config.sync());
const initialCustomFrame: boolean = config.customFrame;
const initialCFNM: boolean = config.customFrameNativeMenu;
mainWindow.webContents.on("did-finish-load", () => {
// send the config
config.sync();
// on macOS add margin to the title, and hide custom controls
// We only use initial values other the menu can disappear
if (process.platform === "darwin" && initialCustomFrame && initialCFNM) {
mainWindow.webContents.insertCSS(`
#root > div[style="display: flex; flex-direction: column; height: 100%;"] > div > div.h_29px {
&> div.d_flex:first-child {
margin-left: 75px;
}
&> a.place-items_center {
display: none;
}
}
`);
}
});
// configure spellchecker context menu
mainWindow.webContents.on("context-menu", (_, params) => {

View file

@ -1,6 +1,6 @@
import { contextBridge, ipcRenderer } from "electron";
import { version } from "../../package.json";
import { aviaVersion, version } from "../../package.json";
contextBridge.exposeInMainWorld("native", {
versions: {
@ -8,6 +8,7 @@ contextBridge.exposeInMainWorld("native", {
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
desktop: () => version,
aviaClient: () => aviaVersion,
},
minimise: () => ipcRenderer.send("minimise"),