├── pnpm-workspace.yaml ├── .gitignore ├── bun.lockb ├── plugins ├── userpfp │ ├── index.scss │ ├── plugin.json │ ├── package.json │ └── index.tsx ├── dorion-updater │ ├── plugin.json │ └── index.tsx ├── platform-spoof │ ├── plugin.json │ ├── package.json │ └── index.tsx ├── shelteRPC │ ├── plugin.json │ ├── package.json │ ├── index.scss │ ├── components │ │ ├── RegisteredGames.scss │ │ ├── GameCard.scss │ │ └── GameCard.tsx │ └── util.ts ├── dorion-tray │ ├── plugin.json │ └── index.ts ├── dorion-ptt │ ├── plugin.json │ └── index.ts ├── dorion-settings │ ├── plugin.json │ ├── pages │ │ ├── SettingsPage.tsx.scss │ │ ├── ThemesPage.tsx.scss │ │ ├── RPC.tsx.scss │ │ ├── ProfilesPage.tsx.scss │ │ ├── PerformancePage.tsx.scss │ │ ├── PluginsPage.tsx.scss │ │ ├── PluginsPage.tsx │ │ ├── ChangelogPage.tsx.scss │ │ ├── ThemesPage.tsx │ │ ├── ProfilesPage.tsx │ │ └── ChangelogPage.tsx │ ├── components │ │ ├── WarningCard.tsx.scss │ │ ├── WarningCard.tsx │ │ ├── PluginList.tsx.scss │ │ ├── ClientModList.tsx.scss │ │ ├── ClientModList.tsx │ │ └── PluginList.tsx │ ├── util │ │ ├── settings.ts │ │ ├── theme.tsx │ │ └── changelog.ts │ ├── types │ │ └── release.ts │ └── index.tsx ├── dorion-titlebar │ ├── plugin.json │ ├── actions.ts │ ├── index.scss │ ├── waitElm.ts │ └── index.tsx ├── orbolay │ ├── plugin.json │ ├── settings.scss │ └── settings.tsx ├── plugin-browser │ ├── plugin.json │ ├── types.d.ts │ ├── index.ts │ ├── components │ │ ├── PluginCard.scss │ │ ├── Plugins.scss │ │ ├── PluginCard.tsx │ │ └── Plugins.tsx │ ├── README.md │ ├── storage.ts │ └── github.ts ├── always-trust │ ├── plugin.json │ └── index.ts ├── dorion-fullscreen │ ├── plugin.json │ └── index.tsx ├── inline-css │ ├── plugin.json │ ├── package.json │ ├── index.tsx │ └── components │ │ ├── Editor.scss │ │ ├── Window.scss │ │ ├── Close.tsx │ │ ├── Popout.tsx │ │ ├── Editor.tsx │ │ └── Window.tsx ├── no-reply-mention │ ├── plugin.json │ └── index.tsx ├── youre-right │ ├── plugin.json │ ├── index.tsx.scss │ └── index.tsx ├── dorion-notifications │ ├── plugin.json │ └── index.tsx ├── invisible-typing │ ├── plugin.json │ ├── index.scss │ └── index.tsx ├── dorion-streamer-mode │ ├── plugin.json │ └── index.tsx ├── dorion-custom-keybinds │ ├── plugin.json │ ├── package.json │ ├── components │ │ ├── KeybindSection.tsx.scss │ │ ├── Keybinds.tsx.scss │ │ ├── KeybindSection.tsx │ │ └── Keybinds.tsx │ ├── util │ │ ├── events.ts │ │ └── actionMap.ts │ └── index.tsx ├── dorion-theme-browser │ ├── plugin.json │ ├── index.ts │ ├── components │ │ ├── ThemeCard.tsx.scss │ │ ├── ThemePage.tsx.scss │ │ ├── ThemeCard.tsx │ │ └── ThemePage.tsx │ └── api.ts ├── dorion-voice-fix │ ├── plugin.json │ └── index.tsx ├── openasar-dom-optimizer │ ├── plugin.json │ └── index.tsx ├── dorion-helpers │ ├── plugin.json │ └── index.tsx ├── web-keybinds │ ├── plugin.json │ └── index.tsx ├── blur-nsfw │ ├── plugin.json │ └── index.ts └── clean-home │ ├── plugin.json │ └── index.tsx ├── components ├── Card.tsx.scss ├── RadioGroup.tsx.scss ├── SelectArrow.tsx ├── Card.tsx ├── RadioGroup.tsx ├── Radio.tsx ├── Dropdown.tsx.scss ├── Radio.tsx.scss ├── Dropdown.tsx ├── KeybindInput.tsx.scss └── KeybindInput.tsx ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── lint.yml │ └── deploy.yml ├── util ├── debounce.ts ├── modal.tsx ├── theme.ts └── keyUtil.ts ├── css.d.ts ├── api ├── none.ts ├── flooed.ts ├── dorion.ts └── api.ts ├── tsconfig.json ├── lune.config.js ├── package.json ├── eslint.config.mjs ├── types.d.ts └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - plugins/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .pnpm-debug.log 4 | .idea 5 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpikeHD/shelter-plugins/HEAD/bun.lockb -------------------------------------------------------------------------------- /plugins/userpfp/index.scss: -------------------------------------------------------------------------------- 1 | .submit { 2 | margin-bottom: 10px; 3 | display: inline-block; 4 | } -------------------------------------------------------------------------------- /components/Card.tsx.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | border: 1px solid var(--background-base-lowest); 3 | border-radius: 4px; 4 | } -------------------------------------------------------------------------------- /components/RadioGroup.tsx.scss: -------------------------------------------------------------------------------- 1 | .radioGroupItem { 2 | &:not(:last-child) { 3 | margin-bottom: 16px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /plugins/userpfp/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UserPFP", 3 | "author": "SpikeHD", 4 | "description": "Custom animated profiles." 5 | } -------------------------------------------------------------------------------- /plugins/dorion-updater/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Updater", 3 | "author": "SpikeHD", 4 | "description": "Updater for Dorion." 5 | } -------------------------------------------------------------------------------- /plugins/platform-spoof/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Platform Spoof", 3 | "author": "SpikeHD", 4 | "description": "Spoof your platform." 5 | } -------------------------------------------------------------------------------- /plugins/shelteRPC/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shelteRPC", 3 | "author": "SpikeHD", 4 | "description": "arRPC implementation for Shelter" 5 | } -------------------------------------------------------------------------------- /plugins/dorion-tray/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Tray", 3 | "author": "Luximus", 4 | "description": "Adds live tray icons to Dorion." 5 | } -------------------------------------------------------------------------------- /plugins/dorion-ptt/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion PTT", 3 | "author": "SpikeHD", 4 | "description": "Enables push-to-talk on the Dorion client." 5 | } -------------------------------------------------------------------------------- /plugins/shelteRPC/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shelterpc", 3 | "dependencies": { 4 | "@cumjar/websmack": "^1.2.0" 5 | }, 6 | "type": "module" 7 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Settings", 3 | "author": "SpikeHD", 4 | "description": "Settings page for the Dorion client." 5 | } -------------------------------------------------------------------------------- /plugins/dorion-titlebar/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Titlebar", 3 | "author": "SpikeHD", 4 | "description": "Functionality for Dorion's titlebar" 5 | } -------------------------------------------------------------------------------- /plugins/orbolay/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Orbolay Bridge", 3 | "author": "SpikeHD", 4 | "description": "Bridge plugin for interacting with Orbolay" 5 | } -------------------------------------------------------------------------------- /plugins/plugin-browser/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Plugin Browser", 3 | "author": "SpikeHD", 4 | "description": "Find many shelter plugins in one place!" 5 | } -------------------------------------------------------------------------------- /plugins/userpfp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "userpfp", 3 | "dependencies": { 4 | "@cumjar/websmack": "^1.2.0", 5 | "spitroast": "^1.4.4" 6 | } 7 | } -------------------------------------------------------------------------------- /plugins/always-trust/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Always Trust", 3 | "author": "SpikeHD", 4 | "description": "Remove the \"You are leaving Discord\" popup" 5 | } -------------------------------------------------------------------------------- /plugins/dorion-fullscreen/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Fullscreen", 3 | "author": "SpikeHD", 4 | "description": "Tweak Fullscreen behaviour in Dorion." 5 | } -------------------------------------------------------------------------------- /plugins/inline-css/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Inline CSS", 3 | "author": "SpikeHD", 4 | "description": "Dead-simple inline CSS editor with hot-reloading." 5 | } -------------------------------------------------------------------------------- /plugins/no-reply-mention/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "No Reply Mention", 3 | "author": "SpikeHD", 4 | "description": "Disables mentions on replies by default." 5 | } -------------------------------------------------------------------------------- /plugins/youre-right/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "You're Right", 3 | "description": "Float your message to the right instead of the left.", 4 | "author": "SpikeHD" 5 | } -------------------------------------------------------------------------------- /plugins/dorion-notifications/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Notifications", 3 | "author": "SpikeHD", 4 | "description": "Notification fixes for the Dorion client." 5 | } -------------------------------------------------------------------------------- /plugins/invisible-typing/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Invisible Typing", 3 | "author": "SpikeHD", 4 | "description": "Prevents others from seeing when you are typing." 5 | } -------------------------------------------------------------------------------- /plugins/platform-spoof/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platform-spoof", 3 | "dependencies": { 4 | "@cumjar/websmack": "^1.2.0", 5 | "spitroast": "^1.4.4" 6 | } 7 | } -------------------------------------------------------------------------------- /plugins/dorion-streamer-mode/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Streamer Mode", 3 | "author": "SpikeHD", 4 | "description": "Streamer mode detection for the Dorion client." 5 | } -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Custom Keybinds", 3 | "description": "Enable the use of global, customized keybinds", 4 | "author": "SpikeHD" 5 | } -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Theme Browser", 3 | "description": "Browse and install themes directly within Dorion", 4 | "author": "SpikeHD" 5 | } -------------------------------------------------------------------------------- /plugins/dorion-voice-fix/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Voice Fix", 3 | "author": "SpikeHD", 4 | "description": "Force various things to be \"supported\" regarding voice." 5 | } -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dorion-custom-keybinds", 3 | "dependencies": { 4 | "@cumjar/websmack": "^1.2.0", 5 | "spitroast": "^1.4.4" 6 | } 7 | } -------------------------------------------------------------------------------- /plugins/openasar-dom-optimizer/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenAsar DOM Optimizer", 3 | "author": "OpenAsar Contributors & SpikeHD", 4 | "description": "Optimize channel and server switching!" 5 | } -------------------------------------------------------------------------------- /plugins/inline-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inline-css", 3 | "dependencies": { 4 | "@srsholmes/solid-code-input": "^0.0.18", 5 | "highlight.js": "^11.9.0" 6 | }, 7 | "type": "module" 8 | } -------------------------------------------------------------------------------- /plugins/dorion-helpers/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dorion Helpers", 3 | "author": "SpikeHD", 4 | "description": "A bunch of very small chunks of functionality that didn't deserve their own plugins." 5 | } 6 | -------------------------------------------------------------------------------- /plugins/web-keybinds/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web Keybinds", 3 | "author": "SpikeHD & Vencord Contributors", 4 | "description": "(Incomplete) port of Vencord's \"WebKeybinds\" plugin. PRs open!" 5 | } 6 | -------------------------------------------------------------------------------- /plugins/blur-nsfw/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blur NSFW", 3 | "author": "SpikeHD", 4 | "description": "Blur images and videos in NSFW channels. Hover to unblur, clicking to preview will also be unblurred." 5 | } -------------------------------------------------------------------------------- /plugins/clean-home/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Declutter", 3 | "author": "SpikeHD", 4 | "description": "Hide/disable unwanted or distracting components, such as Nitro effects, Store/Nitro tabs, and much more." 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.gg/agQ9mRdHMZ 5 | about: Ask anything non-bug/feature related here (such as "How does XYZ work?" or "Why does ZYX work the way it does?") 6 | -------------------------------------------------------------------------------- /plugins/shelteRPC/index.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: space-between; 6 | 7 | margin-bottom: 12px; 8 | } 9 | 10 | .container input[type=number] { 11 | width: 30%; 12 | flex-grow: 0; 13 | } -------------------------------------------------------------------------------- /plugins/always-trust/index.ts: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | stores: { 4 | MaskedLinkStore 5 | } 6 | }, 7 | patcher 8 | } = shelter 9 | 10 | const unpatch = patcher.instead('isTrustedDomain', MaskedLinkStore, () => true, false) 11 | 12 | export const onUnload = () => { 13 | unpatch() 14 | } 15 | -------------------------------------------------------------------------------- /plugins/orbolay/settings.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: space-between; 6 | 7 | margin: 12px 0; 8 | } 9 | 10 | .container > span { 11 | font-weight: 500; 12 | } 13 | 14 | .container > * { 15 | width: 50% !important; 16 | } -------------------------------------------------------------------------------- /plugins/plugin-browser/types.d.ts: -------------------------------------------------------------------------------- 1 | interface Repo { 2 | name: string 3 | description: string 4 | url: string 5 | stars: number 6 | owner: string 7 | owner_url: string 8 | owner_avatar: string 9 | homepage: string 10 | } 11 | 12 | interface RepoInfo { 13 | repo: Repo 14 | site: string 15 | plugins: string[] 16 | } -------------------------------------------------------------------------------- /util/debounce.ts: -------------------------------------------------------------------------------- 1 | export const debounce = (fn: (...args: unknown[]) => unknown, delay: number) => { 2 | let timer: number | null = null 3 | return (...args: unknown[]) => { 4 | if (timer) { 5 | clearTimeout(timer) 6 | } 7 | timer = setTimeout(() => { 8 | fn.apply(this, args) 9 | }, delay) 10 | } 11 | } -------------------------------------------------------------------------------- /components/SelectArrow.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | class?: string; 3 | } 4 | 5 | export const SelectArrow = (props: Props) => ( 6 | 7 | 8 | 9 | ) -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/SettingsPage.tsx.scss: -------------------------------------------------------------------------------- 1 | .tophead { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .shead { 6 | margin-top: 16px; 7 | margin-bottom: 8px; 8 | font-weight: bold; 9 | } 10 | 11 | .ohead { 12 | margin-top: 16px; 13 | margin-bottom: 8px; 14 | font-weight: 500; 15 | } 16 | 17 | .left16 { 18 | margin-left: 16px; 19 | } 20 | -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/ThemesPage.tsx.scss: -------------------------------------------------------------------------------- 1 | .tophead { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .shead { 6 | margin-top: 16px; 7 | margin-bottom: 8px; 8 | font-weight: bold; 9 | } 10 | 11 | .pbuttons { 12 | display: flex; 13 | gap: 16px; 14 | margin-top: 16px; 15 | 16 | button { 17 | width: 100% !important; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /css.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | export const css: string 3 | export const classes: Record 4 | } 5 | 6 | declare module '*.scss' { 7 | export const css: string 8 | export const classes: Record 9 | } 10 | 11 | declare module '*.sass' { 12 | export const css: string 13 | export const classes: Record 14 | } -------------------------------------------------------------------------------- /plugins/shelteRPC/components/RegisteredGames.scss: -------------------------------------------------------------------------------- 1 | .description { 2 | margin-top: 8px; 3 | margin-bottom: 8px; 4 | } 5 | 6 | .addIt { 7 | margin-top: 8px; 8 | margin-bottom: 28px; 9 | } 10 | 11 | .tophead { 12 | margin-bottom: 12px; 13 | } 14 | 15 | .shead { 16 | margin-top: 42px; 17 | margin-bottom: 12px; 18 | font-weight: bold; 19 | } 20 | 21 | .modalhead { 22 | margin-top: 12px; 23 | } -------------------------------------------------------------------------------- /api/none.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Unknown', 3 | 4 | /* stub */ 5 | invoke: async () => {}, 6 | event: { 7 | emit: () => {}, 8 | listen: async () => {} 9 | }, 10 | app: { 11 | getVersion: () => '0.0.0' 12 | }, 13 | process: { 14 | relaunch: () => {} 15 | }, 16 | apiWindow: { 17 | appWindow: { 18 | setFullscreen: () => {} 19 | } 20 | } 21 | } satisfies Backend -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/RPC.tsx.scss: -------------------------------------------------------------------------------- 1 | .shead { 2 | margin-top: 16px; 3 | margin-bottom: 8px; 4 | font-weight: bold; 5 | } 6 | 7 | .bot16 { 8 | margin-bottom: 16px; 9 | } 10 | 11 | .customInstallBtn { 12 | width: 100%; 13 | margin: 20px 0; 14 | padding: 20px; 15 | } 16 | 17 | .customNote { 18 | color: var(--text-subtle) !important; 19 | font-weight: 400; 20 | font-size: 14px; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "types": ["./types.d.ts", "./css.d.ts"], 5 | "target": "es6", 6 | "lib": ["es2020", "dom"], 7 | "module": "nodenext", 8 | "moduleResolution": "nodenext" 9 | }, 10 | "include": ["plugins/**/*.ts", "plugins/**/*.tsx", "node_modules/@uwu/shelter-defs/**/*.d.ts", "plugins/clean-home/index.tsx", "util/keyUtil.ts", "util/modal.tsx"] 11 | } -------------------------------------------------------------------------------- /plugins/plugin-browser/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugins } from './components/Plugins.jsx' 2 | import { createLocalStorage } from './storage.js' 3 | 4 | const { 5 | settings: { 6 | registerSection, 7 | }, 8 | } = shelter 9 | 10 | const unload = registerSection('section', 'plugin-browser', 'Plugin Browser', Plugins) 11 | 12 | if (!window.localStorage) { 13 | createLocalStorage() 14 | } 15 | 16 | export const onUnload = () => { 17 | unload() 18 | } -------------------------------------------------------------------------------- /lune.config.js: -------------------------------------------------------------------------------- 1 | // Welcome to your Lune config file! 2 | // You can view documentation on Lune here: 3 | // https://github.com/uwu/shelter/tree/main/packages/lune 4 | // uncomment lines below to enable options, and feel free to delete this header. 5 | import { defineConfig } from '@uwu/lune' 6 | 7 | export default defineConfig({ 8 | repoSubDir: 'plugins', 9 | 10 | // this enables CSS Module support - see docs for info 11 | cssModules: 'legacy', 12 | }) 13 | -------------------------------------------------------------------------------- /plugins/dorion-settings/components/WarningCard.tsx.scss: -------------------------------------------------------------------------------- 1 | 2 | .restartCard { 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: space-around; 6 | align-items: center; 7 | 8 | border: 1px solid var(--status-warning) !important; 9 | background-color: var(--info-warning-background); 10 | padding: 16px; 11 | } 12 | 13 | .restartButton { 14 | width: 100% !important; 15 | background-color: var(--status-warning) !important; 16 | margin-top: 8px; 17 | } 18 | -------------------------------------------------------------------------------- /plugins/dorion-voice-fix/index.tsx: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | stores: { 4 | MediaEngineStore 5 | } 6 | }, 7 | patcher 8 | } = shelter 9 | 10 | const unpatches = [ 11 | patcher.instead('isSupported', MediaEngineStore, () => true), 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | patcher.instead('supports', MediaEngineStore, (_e: string) => true), 14 | ] 15 | 16 | export const onUnload = () => { 17 | unpatches.forEach(unpatch => unpatch()) 18 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/ProfilesPage.tsx.scss: -------------------------------------------------------------------------------- 1 | .tophead { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .shead { 6 | margin-top: 16px; 7 | margin-bottom: 8px; 8 | font-weight: bold; 9 | } 10 | 11 | .sbutton { 12 | margin-top: 16px; 13 | padding: 18px; 14 | 15 | width: 100%; 16 | } 17 | 18 | .splitbutton { 19 | width: 100%; 20 | } 21 | 22 | .pbuttons { 23 | display: flex; 24 | gap: 16px; 25 | margin-top: 16px; 26 | 27 | button { 28 | width: 100% !important; 29 | } 30 | } -------------------------------------------------------------------------------- /plugins/dorion-streamer-mode/index.tsx: -------------------------------------------------------------------------------- 1 | import { event } from '../../api/api.js' 2 | 3 | const { 4 | flux: { 5 | dispatcher: FluxDispatcher 6 | } 7 | } = shelter 8 | 9 | 10 | const unlisten = event.listen('streamer_mode_toggle', (event) => { 11 | const enabled = event.payload 12 | 13 | FluxDispatcher.dispatch({ 14 | type: 'STREAMER_MODE_UPDATE', 15 | key: 'enabled', 16 | value: enabled, 17 | }) 18 | }) 19 | 20 | export const onUnload = () => { 21 | unlisten() 22 | } 23 | -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/index.ts: -------------------------------------------------------------------------------- 1 | import { appName } from '../../api/api.js' 2 | import { ThemePage } from './components/ThemePage.jsx' 3 | 4 | const { 5 | settings: { 6 | registerSection, 7 | }, 8 | } = shelter 9 | 10 | const uninjects = [ 11 | registerSection('divider'), 12 | registerSection('header', 'Theme Browser'), 13 | registerSection('section', `${appName}-theme-browser`, 'Theme Browser', ThemePage), 14 | ] 15 | 16 | export const onUnload = () => { 17 | uninjects.forEach((u) => u()) 18 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/PerformancePage.tsx.scss: -------------------------------------------------------------------------------- 1 | .tophead { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .shead { 6 | margin-top: 16px; 7 | margin-bottom: 8px; 8 | font-weight: bold; 9 | } 10 | 11 | .stext { 12 | color: var(--text-subtle) !important; 13 | font-size: 14px; 14 | margin: 12px 0; 15 | } 16 | 17 | .pbuttons { 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | justify-content: space-between; 22 | gap: 16px; 23 | width: 100%; 24 | margin-top: 16px; 25 | } 26 | -------------------------------------------------------------------------------- /components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './Card.tsx.scss' 2 | 3 | const { 4 | ui: { injectCss }, 5 | } = shelter 6 | 7 | let injectedCss = false 8 | 9 | export const Card: Component<{ 10 | title: string; 11 | children?: any; 12 | style?: any; 13 | class?: string; 14 | }> = (props) => { 15 | if (!injectedCss) { 16 | injectedCss = true 17 | injectCss(css) 18 | } 19 | 20 | return ( 21 | 22 | { props.children } 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /plugins/invisible-typing/index.scss: -------------------------------------------------------------------------------- 1 | .invisContainer { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | 6 | width: var(--space-32); 7 | height: var(--space-32); 8 | 9 | cursor: pointer; 10 | 11 | /* We have to apply to the SVG because Discord hits us with a !important */ 12 | svg path { 13 | fill: var(--interactive-normal) !important; 14 | } 15 | 16 | &.notShowing svg path { 17 | fill: var(--status-danger) !important; 18 | } 19 | } 20 | 21 | .invisContainer svg { 22 | height: 55%; 23 | width: 100%; 24 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/PluginsPage.tsx.scss: -------------------------------------------------------------------------------- 1 | .tophead { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .shead { 6 | margin-top: 16px; 7 | margin-bottom: 8px; 8 | font-weight: bold; 9 | } 10 | 11 | .card { 12 | margin-bottom: 16px; 13 | } 14 | 15 | .left16 { 16 | margin-left: 16px; 17 | } 18 | 19 | .openButton { 20 | margin-top: 16px; 21 | width: 100% !important; 22 | } 23 | 24 | .pbuttons { 25 | display: flex; 26 | flex-direction: row; 27 | align-items: center; 28 | justify-content: space-between; 29 | gap: 16px; 30 | width: 100%; 31 | margin-top: 16px; 32 | } -------------------------------------------------------------------------------- /plugins/plugin-browser/components/PluginCard.scss: -------------------------------------------------------------------------------- 1 | .pluginCard { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-evenly; 5 | align-items: flex-start; 6 | text-align: left; 7 | 8 | padding: 16px; 9 | 10 | color: var(--text-default); 11 | background: var(--background-surface-highest); 12 | 13 | border-radius: 8px; 14 | 15 | .contents { 16 | margin-top: 8px; 17 | flex: 1; 18 | } 19 | 20 | .buttonContainer { 21 | margin-top: 8px; 22 | width: 100%; 23 | 24 | .installButton { 25 | flex-grow: 1; 26 | width: 100%; 27 | 28 | button P{ 29 | width: 100%; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help me improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Describe the problem here. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | Describe what should be expected here. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **System Info (please complete the following information):** 23 | - OS: [e.g. Windows 11, Ubuntu 22.04] 24 | - Discord version [e.g. Regular, Canary, PTB] 25 | - Any other relevant info: 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /components/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Radio } from './Radio.jsx' 2 | import { css, classes } from './Radio.tsx.scss' 3 | 4 | const { 5 | ui: { injectCss }, 6 | } = shelter 7 | 8 | let injectedCss = false 9 | 10 | export const RadioGroup: Component<{ 11 | onChange: (value: string) => void; 12 | selected: string; 13 | options: string[]; 14 | }> = (props) => { 15 | if (!injectedCss) { 16 | injectedCss = true 17 | injectCss(css) 18 | } 19 | 20 | return ( 21 | 22 | {props.options.map((o) => ( 23 | 24 | ))} 25 | 26 | ) 27 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/util/settings.ts: -------------------------------------------------------------------------------- 1 | export const defaultConfig = { 2 | theme: 'none', 3 | 4 | themes: [], 5 | zoom: '1.0', 6 | client_type: 'default', 7 | sys_tray: false, 8 | push_to_talk: false, 9 | push_to_talk_keys: ['RControl'], 10 | cache_css: false, 11 | use_native_titlebar: false, 12 | start_maximized: false, 13 | profile: 'default', 14 | streamer_mode_detection: false, 15 | rpc_server: false, 16 | open_on_startup: false, 17 | startup_minimized: false, 18 | autoupdate: false, 19 | update_notify: true, 20 | desktop_notifications: false, 21 | auto_clear_cache: false, 22 | multi_instance: false, 23 | disable_hardware_accel: false, 24 | blur: 'none', 25 | blur_css: true, 26 | client_mods: ['Shelter'], 27 | unread_badge: true, 28 | } -------------------------------------------------------------------------------- /plugins/plugin-browser/components/Plugins.scss: -------------------------------------------------------------------------------- 1 | .subtitle { 2 | margin-top: 12px; 3 | display: block; 4 | } 5 | 6 | .pluginList { 7 | display: grid; 8 | grid-template-columns: repeat(2, 1fr); 9 | grid-gap: 16px; 10 | margin-top: 16px; 11 | } 12 | 13 | .repoHeader { 14 | display: flex; 15 | flex-direction: row; 16 | justify-content: space-between; 17 | align-items: center; 18 | } 19 | 20 | .loading { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | height: 100%; 25 | 26 | margin-top: 12px; 27 | } 28 | 29 | .split { 30 | display: flex; 31 | flex-direction: row; 32 | justify-content: space-between; 33 | align-items: center; 34 | 35 | height: 40px; 36 | } 37 | 38 | .split button { 39 | height: 100%; 40 | width: 10%; 41 | } -------------------------------------------------------------------------------- /util/modal.tsx: -------------------------------------------------------------------------------- 1 | const { 2 | ui: { 3 | ModalRoot, 4 | ModalHeader, 5 | ModalBody, 6 | ModalConfirmFooter, 7 | } 8 | } = shelter 9 | 10 | export const basicModal = (props: BasicModalProps) => ( 11 | 12 | {props.header} 13 | {props.body} 14 | 15 | ) 16 | 17 | export const confirmModal = (props: ConfirmationModalProps) => ( 18 | 19 | {props.header} 22 | {props.body} 23 | 30 | 31 | ) -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.lock' 7 | pull_request: 8 | paths-ignore: 9 | - '**.lock' 10 | 11 | concurrency: 12 | group: ${{ github.ref }}-${{ github.workflow }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | CARGO_INCREMENTAL: 0 17 | 18 | jobs: 19 | tsc-eslint-checks: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Install Node 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: 20 26 | 27 | - uses: actions/checkout@v4 28 | - uses: pnpm/action-setup@v2 29 | with: 30 | version: 8.6.7 31 | 32 | - name: Install modules 33 | run: pnpm install 34 | 35 | - name: Run tsc 36 | run: pnpm lune ci 37 | 38 | - name: Run ESLint 39 | run: pnpm lint 40 | -------------------------------------------------------------------------------- /api/flooed.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Flooed', 3 | 4 | invoke: (name: string, payload?: any) => { 5 | return window.Flooed.invoke(name, payload) 6 | }, 7 | event: { 8 | // emit: (name: string, payload: any) => { 9 | // return 10 | // }, 11 | // listen: async (name: string, callback: (payload: any) => void) => { 12 | // return 13 | // } 14 | emit: () => {}, 15 | listen: async () => {} 16 | }, 17 | app: { 18 | getVersion: () => { 19 | return window.Flooed.version 20 | } 21 | }, 22 | process: { 23 | relaunch: () => { 24 | return window.Flooed.invoke('relaunch') 25 | } 26 | }, 27 | apiWindow: { 28 | appWindow: { 29 | setFullscreen: (isFullscreen: boolean) => { 30 | return window.Flooed.invoke('set_fullscreen', isFullscreen) 31 | } 32 | } 33 | } 34 | } satisfies Backend -------------------------------------------------------------------------------- /components/Radio.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './Radio.tsx.scss' 2 | 3 | const { 4 | ui: { injectCss, Text }, 5 | } = shelter 6 | 7 | let injectedCss = false 8 | 9 | export const Radio: Component<{ 10 | onClick: (value: string) => void; 11 | value: string; 12 | label: string; 13 | selected: boolean; 14 | }> = (props) => { 15 | if (!injectedCss) { 16 | injectedCss = true 17 | injectCss(css) 18 | } 19 | 20 | const onRadioClick = () => { 21 | props.onClick(props.value) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {props.selected && } 28 | 29 | {props.label} 30 | 31 | ) 32 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@eslint/eslintrc": "^3.3.3", 4 | "@eslint/js": "^9.39.1", 5 | "@tauri-apps/api": "^1.6.0", 6 | "@uwu/lune": "^1.5.1", 7 | "@uwu/shelter-defs": "^1.4.1", 8 | "globals": "^15.15.0", 9 | "typescript": "^5.9.3" 10 | }, 11 | "scripts": { 12 | "lint": "eslint ./plugins ./components", 13 | "lint:fix": "eslint --fix ./plugins ./components" 14 | }, 15 | "type": "module", 16 | "workspaces": [ 17 | "plugins/*" 18 | ], 19 | "dependencies": { 20 | "@typescript-eslint/eslint-plugin": "latest", 21 | "@typescript-eslint/parser": "latest", 22 | "eslint": "latest", 23 | "marked": "^11.2.0", 24 | "spitroast": "^1.4.4" 25 | }, 26 | "packageManager": "pnpm@10.6.2+sha512.47870716bea1572b53df34ad8647b42962bc790ce2bf4562ba0f643237d7302a3d6a8ecef9e4bdfc01d23af1969aa90485d4cebb0b9638fa5ef1daef656f6c1b" 27 | } 28 | -------------------------------------------------------------------------------- /plugins/dorion-settings/components/WarningCard.tsx: -------------------------------------------------------------------------------- 1 | import { process } from '../../../api/api.js' 2 | import { Card } from '../../../components/Card.jsx' 3 | import { css, classes } from './WarningCard.tsx.scss' 4 | 5 | const { 6 | ui: { 7 | injectCss, 8 | Text, 9 | Button 10 | } 11 | } = shelter 12 | 13 | let injectedCss = false 14 | 15 | export function WarningCard() { 16 | if (!injectedCss) { 17 | injectedCss = true 18 | injectCss(css) 19 | } 20 | 21 | return ( 22 | 23 | 24 | One or more settings have been changed that require a restart to take effect. 25 | 26 | process.relaunch()} 28 | class={classes.restartButton} 29 | grow={true} 30 | > 31 | Restart 32 | 33 | 34 | ) 35 | } -------------------------------------------------------------------------------- /plugins/dorion-titlebar/actions.ts: -------------------------------------------------------------------------------- 1 | import { classes } from './index.scss' 2 | 3 | export function close() { 4 | // @ts-expect-error shut up 5 | window.__TAURI__.core.invoke('close') 6 | } 7 | 8 | export function minimize() { 9 | // @ts-expect-error shut up 10 | window.__TAURI__.core.invoke('minimize') 11 | } 12 | 13 | export function toggleMaximize() { 14 | // @ts-expect-error shut up 15 | window.__TAURI__.core.invoke('toggle_maximize') 16 | } 17 | 18 | export async function setMaximizeIcon() { 19 | // @ts-expect-error shut up 20 | if (await window?.__TAURI__?.webviewWindow.getCurrentWebviewWindow().isMaximized()) { 21 | const topmax = document.querySelector(`.${classes.topmax}`) as HTMLDivElement 22 | topmax?.classList?.add(classes.maximized) 23 | } else { 24 | const topmax = document.querySelector(`.${classes.topmax}`) as HTMLDivElement 25 | topmax?.classList?.remove(classes.maximized) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/Dropdown.tsx.scss: -------------------------------------------------------------------------------- 1 | .ddown { 2 | box-sizing: border-box; 3 | 4 | font-size: 16px; 5 | width: 100%; 6 | border-radius: 4px; 7 | color: var(--text-default); 8 | background-color: var(--background-base-lowest); 9 | border: none; 10 | transition: border-color 0.2s ease-in-out; 11 | padding: 10px; 12 | 13 | appearance: none; 14 | 15 | cursor: pointer; 16 | } 17 | 18 | .ddown option { 19 | /* TODO --background-base-lowest doesn't seem to work here (at least on Windows). Oh well. */ 20 | background: #333; 21 | color: var(--text-default); 22 | } 23 | 24 | .dcontainer { 25 | position: relative; 26 | width: 100%; 27 | } 28 | 29 | .dsarrow { 30 | position: absolute; 31 | right: 10px; 32 | top: 50%; 33 | transform: translateY(-50%); 34 | pointer-events: none; 35 | } 36 | 37 | .dsarrow path { 38 | fill: var(--text-subtle); 39 | } 40 | 41 | .ddownplaceholder { 42 | color: var(--text-subtle); 43 | } 44 | -------------------------------------------------------------------------------- /plugins/no-reply-mention/index.tsx: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | intercept, 4 | stores: { 5 | UserStore 6 | } 7 | }, 8 | ui: { 9 | SwitchItem 10 | }, 11 | plugin: { 12 | store 13 | } 14 | } = shelter 15 | 16 | const unintercept = intercept(dispatch => { 17 | if (dispatch.type !== 'CREATE_PENDING_REPLY') return 18 | 19 | // @ts-expect-error cry about it 20 | const userIsAuthor = dispatch?.message?.author?.id === UserStore.getCurrentUser()?.id 21 | 22 | dispatch.shouldMention = (store.shiftToInvert && !userIsAuthor) ? !dispatch.shouldMention : false 23 | }) 24 | 25 | export const onUnload = () => { 26 | unintercept() 27 | } 28 | 29 | export const settings = () => ( 30 | <> 31 | { 34 | store.shiftToInvert = v 35 | }} 36 | note="Enable to make holding shift enable mentions." 37 | > 38 | Inverse Shift Reply 39 | 40 | > 41 | ) 42 | -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/components/KeybindSection.tsx.scss: -------------------------------------------------------------------------------- 1 | .keybindRoot { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | margin-bottom: 16px; 6 | } 7 | 8 | .keybindSection { 9 | width: 100%; 10 | display: flex; 11 | flex-direction: row; 12 | justify-content: space-between; 13 | align-items: center; 14 | } 15 | 16 | .actionSection, 17 | .keybindArea { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .actionSection { 23 | width: 50%; 24 | } 25 | 26 | .removeButton { 27 | width: 10%; 28 | height: 20px; 29 | 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | 34 | /* hide by default */ 35 | opacity: 0; 36 | 37 | transition: all 0.1s ease-in-out; 38 | cursor: pointer; 39 | } 40 | 41 | .keybindRoot:hover .removeButton { 42 | opacity: 1; 43 | } 44 | 45 | .keybindArea { 46 | width: 50%; 47 | } 48 | 49 | .note { 50 | margin-top: 8px; 51 | color: var(--text-subtle) !important; 52 | font-size: 14px; 53 | line-height: 20px; 54 | font-weight: 400; 55 | } 56 | -------------------------------------------------------------------------------- /plugins/shelteRPC/util.ts: -------------------------------------------------------------------------------- 1 | const baseUrl = 'https://cdn.discordapp.com' 2 | 3 | export const generateIconUrl = (appId: string, icon: string) => { 4 | return `${baseUrl}/app-icons/${appId}/${icon}.png` 5 | } 6 | 7 | export const generateCoverUrl = (appId: string, cover: string) => { 8 | return `${baseUrl}/app-icons/${appId}/${cover}.png` 9 | } 10 | 11 | /** 12 | * Convert the timestamp to something like "2 minutes ago" or "Just now", showing only the largest unit 13 | */ 14 | export const timestampToRelative = (timestamp: number) => { 15 | const now = Date.now() 16 | const diff = now - timestamp 17 | 18 | const seconds = Math.floor(diff / 1000) 19 | const minutes = Math.floor(seconds / 60) 20 | const hours = Math.floor(minutes / 60) 21 | const days = Math.floor(hours / 24) 22 | 23 | if (days > 0) { 24 | return `${days} day${days > 1 ? 's' : ''} ago` 25 | } 26 | 27 | if (hours > 0) { 28 | return `${hours} hour${hours > 1 ? 's' : ''} ago` 29 | } 30 | 31 | if (minutes > 0) { 32 | return `${minutes} minute${minutes > 1 ? 's' : ''} ago` 33 | } 34 | 35 | return 'Just now' 36 | } -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/components/ThemeCard.tsx.scss: -------------------------------------------------------------------------------- 1 | .themeCard { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-around; 5 | align-items: flex-start; 6 | text-align: left; 7 | 8 | padding: 0px; 9 | margin: 8px; 10 | 11 | color: var(--text-default); 12 | background: var(--background-surface-highest); 13 | 14 | border-radius: 8px; 15 | 16 | .thumbnail { 17 | width: 100%; 18 | height: 160px; 19 | overflow: hidden; 20 | 21 | /* TL & TR 8px, others 0px */ 22 | border-radius: 8px 8px 0px 0px; 23 | 24 | /* image is set in the background so we can use cover */ 25 | background-size: cover; 26 | background-position: center; 27 | } 28 | 29 | .info { 30 | display: flex; 31 | flex-direction: column; 32 | 33 | margin-top: 6px; 34 | padding: 16px; 35 | width: 100%; 36 | 37 | text-overflow: ellipsis; 38 | overflow: hidden; 39 | 40 | .name, 41 | .contents, 42 | .installButton { 43 | margin-bottom: 8px; 44 | } 45 | 46 | .installButton { 47 | margin-top: 8px; 48 | width: 100%; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /plugins/inline-css/index.tsx: -------------------------------------------------------------------------------- 1 | import Editor from './components/Editor.jsx' 2 | 3 | const { 4 | settings: { 5 | registerSection, 6 | }, 7 | plugin: { 8 | store 9 | } 10 | } = shelter 11 | 12 | let styleElm: HTMLStyleElement | null = null 13 | 14 | // Silly little styling tweak for the code editor 15 | const style = document.createElement('style') 16 | style.textContent = '.code-highlighted { color: var(--text-default) }' 17 | styleElm = document.body.appendChild(style) 18 | 19 | // Also create another style element to contain the CSS 20 | let inlineStyleElm: HTMLStyleElement | null = null 21 | const inlineStyle = document.createElement('style') 22 | inlineStyle.id = 'inline-css-output' 23 | inlineStyleElm = document.body.appendChild(inlineStyle) 24 | 25 | // Set the initial contents of the inline CSS 26 | inlineStyleElm.textContent = store.inlineCss 27 | 28 | const unload = registerSection('section', 'inline-css', 'CSS Editor', () => Editor({ styleElm: inlineStyleElm })) 29 | 30 | export const onUnload = () => { 31 | unload() 32 | 33 | if (styleElm) { 34 | styleElm.remove() 35 | } 36 | 37 | if (inlineStyleElm) { 38 | inlineStyleElm.remove() 39 | } 40 | } -------------------------------------------------------------------------------- /plugins/dorion-helpers/index.tsx: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | stores: { 4 | GuildReadStateStore, 5 | RelationshipStore, 6 | } 7 | }, 8 | } = shelter 9 | 10 | // https://github.com/Vencord/Vesktop/blob/497c251d722d1feab0d703840114c64db82ebb99/src/renderer/appBadge.ts#L16 11 | const updateNotificationBadge = () => { 12 | if (!window?.Dorion?.shouldShowUnreadBadge) return 13 | 14 | // @ts-expect-error cry 15 | const { invoke } = window.__TAURI__.core 16 | 17 | // @ts-expect-error cry 18 | const unread = GuildReadStateStore.hasAnyUnread() 19 | // @ts-expect-error cry 20 | const mentions = GuildReadStateStore.getTotalMentionCount() 21 | // @ts-expect-error cry 22 | const friendRequests = RelationshipStore.getPendingCount() 23 | const total = friendRequests + mentions 24 | 25 | if (!total && unread) invoke('notification_count', { amount: -1 }) 26 | 27 | invoke('notification_count', { amount: total }) 28 | } 29 | 30 | // @ts-expect-error cry 31 | GuildReadStateStore.addChangeListener(updateNotificationBadge) 32 | // @ts-expect-error cry 33 | RelationshipStore.addChangeListener(updateNotificationBadge) 34 | 35 | // Initial update 36 | updateNotificationBadge() 37 | -------------------------------------------------------------------------------- /plugins/plugin-browser/README.md: -------------------------------------------------------------------------------- 1 | # Where is my plugin repo? 2 | 3 | This plugin searches the term `shelter-plugins` on GitHub. That's it! If your repository doesn't show up, make sure to include "shelter plugins" somewhere in your repo's description or name. 4 | 5 | # Why are my plugins not showing up/showing as uninstallable? 6 | 7 | Since there is zero standardization as to how plugins are shared and deployed, this plugin makes some best-guesses: 8 | 9 | * It looks for a homepage set in your repository's settings. If it finds one, it will use that as the plugin's homepage. 10 | * If it doesn't see one, it assumes a `.github.io` URL, as that is most common. 11 | * It then checks two URL structures for the plugin's repo: 12 | * `/shelter-plugins/` 13 | * `/` 14 | 15 | If neither checks succeed, the plugin will be marked as uninstallable, as it doesn't know where to source it. 16 | 17 | To fix this (for example, if you use a custom domain), just set the `homepage` field in your GitHub repo to where the `shelter-plugin`s live. For example, if you host your plugins at `https://myplugins.dev/files/shelter/`, set the homepage to `https://myplugins.dev/files/shelter`. -------------------------------------------------------------------------------- /plugins/dorion-fullscreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { apiWindow } from '../../api/api.js' 2 | 3 | const { 4 | flux: { 5 | dispatcher: FluxDispatcher 6 | }, 7 | } = shelter 8 | 9 | let isPopout = false 10 | 11 | const toggleFullscreen = async (payload) => { 12 | if (isPopout) return 13 | 14 | const topbar = document.querySelector('#dorion_topbar') as HTMLDivElement 15 | 16 | if (topbar) { 17 | topbar.style.display = payload?.isElementFullscreen ? 'none' : 'initial' 18 | } 19 | 20 | apiWindow.appWindow?.setFullscreen(payload?.isElementFullscreen) 21 | } 22 | 23 | const toggleIsPopout = (toggle: boolean) => { 24 | isPopout = toggle 25 | } 26 | 27 | const popoutOff = () => { 28 | toggleIsPopout(false) 29 | } 30 | 31 | const popoutOn = () => { 32 | toggleIsPopout(true) 33 | } 34 | 35 | FluxDispatcher.subscribe('WINDOW_FULLSCREEN_CHANGE', toggleFullscreen) 36 | FluxDispatcher.subscribe('POPOUT_WINDOW_OPEN', popoutOn) 37 | FluxDispatcher.subscribe('WINDOW_UNLOAD', popoutOff) 38 | 39 | export const onUnload = () => { 40 | FluxDispatcher.unsubscribe('WINDOW_FULLSCREEN_CHANGE', toggleFullscreen) 41 | FluxDispatcher.unsubscribe('POPOUT_WINDOW_OPEN', popoutOn) 42 | FluxDispatcher.unsubscribe('WINDOW_UNLOAD', popoutOff) 43 | } 44 | -------------------------------------------------------------------------------- /plugins/dorion-settings/components/PluginList.tsx.scss: -------------------------------------------------------------------------------- 1 | .plist { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: space-between; 6 | 7 | font-size: 16px; 8 | } 9 | 10 | .pheader { 11 | border-bottom: 1px solid var(--text-subtle); 12 | font-weight: bold; 13 | padding-bottom: 16px; 14 | } 15 | 16 | .pbuttons { 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: space-between; 21 | width: 100%; 22 | 23 | margin-top: 16px; 24 | 25 | & button { 26 | width: 30%; 27 | padding: 18px; 28 | } 29 | } 30 | 31 | .sbutton { 32 | margin-top: 16px; 33 | padding: 18px; 34 | 35 | width: 100%; 36 | } 37 | 38 | .plistrow { 39 | display: flex; 40 | flex-direction: row; 41 | align-items: center; 42 | justify-content: space-between; 43 | width: 100%; 44 | 45 | padding: 16px 0; 46 | } 47 | 48 | .plistrow .scell { 49 | display: flex; 50 | align-items: center; 51 | justify-content: end; 52 | width: 30%; 53 | } 54 | 55 | .plistrow .mcell { 56 | display: flex; 57 | align-items: center; 58 | justify-content: flex-start; 59 | width: 50%; 60 | } 61 | 62 | .left16 { 63 | margin-left: 16px; 64 | } 65 | 66 | .top16 { 67 | margin-top: 16px; 68 | } 69 | 70 | .top32 { 71 | margin-top: 32px; 72 | } 73 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [ 18 | ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), 19 | { 20 | plugins: { 21 | "@typescript-eslint": typescriptEslint, 22 | }, 23 | 24 | languageOptions: { 25 | globals: { 26 | ...globals.browser, 27 | }, 28 | 29 | parser: tsParser, 30 | ecmaVersion: "latest", 31 | sourceType: "module", 32 | }, 33 | 34 | rules: { 35 | indent: ["error", 2], 36 | quotes: ["error", "single"], 37 | semi: ["error", "never"], 38 | "react/react-in-jsx-scope": "off", 39 | "@typescript-eslint/no-explicit-any": "off", 40 | }, 41 | }, 42 | ]; -------------------------------------------------------------------------------- /plugins/inline-css/components/Editor.scss: -------------------------------------------------------------------------------- 1 | .ceditor { 2 | padding: 12px; 3 | margin-top: 28px; 4 | border-radius: 5px; 5 | background: var(--background-base-lowest); 6 | 7 | height: 80vh !important; 8 | } 9 | 10 | .ceditor > div { 11 | height: 100% !important; 12 | } 13 | 14 | .controls { 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: space-between; 18 | align-items: center; 19 | 20 | margin-top: 24px; 21 | 22 | & button { 23 | width: 30%; 24 | } 25 | } 26 | 27 | .popout { 28 | width: 100% !important; 29 | margin-top: 12px; 30 | 31 | & svg { 32 | height: 50%; 33 | width: 30px; 34 | } 35 | } 36 | 37 | .ceditor textarea { 38 | height: 100% !important; 39 | cursor: auto; 40 | 41 | color: transparent; 42 | 43 | &::-webkit-scrollbar-corner { 44 | background: transparent 45 | } 46 | 47 | &::-webkit-scrollbar { 48 | background: transparent; 49 | } 50 | 51 | &::-webkit-scrollbar-track { 52 | background: none; 53 | } 54 | 55 | &::-webkit-scrollbar-thumb { 56 | background: var(--primary-530); 57 | border-radius: 4px; 58 | } 59 | 60 | &::-webkit-scrollbar:horizontal { 61 | height: 8px; 62 | } 63 | 64 | &::-webkit-scrollbar:vertical { 65 | width: 8px; 66 | } 67 | } 68 | 69 | .ceditor div[class*="styles-"] { 70 | height: 100% !important; 71 | width: 100% !important; 72 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/components/ClientModList.tsx.scss: -------------------------------------------------------------------------------- 1 | .shead { 2 | margin-top: 16px; 3 | margin-bottom: 8px; 4 | } 5 | 6 | .plist { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: space-between; 11 | 12 | font-size: 16px; 13 | } 14 | 15 | .pheader { 16 | border-bottom: 1px solid var(--background-base-lowest); 17 | font-weight: bold; 18 | padding-bottom: 16px; 19 | } 20 | 21 | .pbuttons { 22 | display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | justify-content: space-between; 26 | width: 100%; 27 | 28 | margin-top: 16px; 29 | 30 | & button { 31 | width: 30%; 32 | padding: 18px; 33 | } 34 | } 35 | 36 | .sbutton { 37 | margin-top: 16px; 38 | padding: 18px; 39 | 40 | width: 100%; 41 | } 42 | 43 | .plistrow { 44 | display: flex; 45 | flex-direction: row; 46 | align-items: center; 47 | justify-content: space-between; 48 | width: 100%; 49 | 50 | padding: 16px; 51 | } 52 | 53 | .plistrow .scell { 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | width: 30%; 58 | } 59 | 60 | .plistrow .mcell { 61 | display: flex; 62 | align-items: center; 63 | justify-content: flex-start; 64 | width: 50%; 65 | } 66 | 67 | .left16 { 68 | margin-left: 16px; 69 | } 70 | 71 | .top16 { 72 | margin-top: 16px; 73 | } 74 | 75 | .top32 { 76 | margin-top: 32px; 77 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/util/theme.tsx: -------------------------------------------------------------------------------- 1 | import { installAndLoad } from '../../../util/theme.js' 2 | import { confirmModal } from '../../../util/modal.jsx' 3 | 4 | const { 5 | ui: { 6 | openModal, 7 | TextBox, 8 | Text 9 | }, 10 | solid: { 11 | createSignal, 12 | } 13 | } = shelter 14 | 15 | export const installThemeModal = async (addToList: (string) => void) => { 16 | const [link, setLink] = createSignal('') 17 | const [status, setStatus] = createSignal('') 18 | 19 | openModal((props) => confirmModal({ 20 | header: 'Install Theme', 21 | body: ( 22 | 23 | setLink(v)} 26 | placeholder={'https://raw.githubusercontent.com/.../theme.css'} 27 | /> 28 | 29 | 35 | {status()} 36 | 37 | 38 | ), 39 | confirmText: 'Install', 40 | type: 'neutral', 41 | onConfirm: async () => { 42 | const themeName = await installAndLoad(link(), setStatus).catch(e => { 43 | setStatus(e) 44 | }) 45 | 46 | addToList(themeName) 47 | 48 | props.close() 49 | }, 50 | onCancel: props.close, 51 | })) 52 | } 53 | -------------------------------------------------------------------------------- /components/Radio.tsx.scss: -------------------------------------------------------------------------------- 1 | .radio { 2 | color: var(--interactive-normal); 3 | grid-template-columns: auto 1fr; 4 | box-sizing: border-box; 5 | border-radius: 4px; 6 | display: grid; 7 | grid-gap: 8px; 8 | align-items: center; 9 | padding: 8px; 10 | background: transparent; 11 | 12 | cursor: pointer; 13 | 14 | &:not(:last-child) { 15 | margin-bottom: 8px; 16 | } 17 | 18 | .radioButton { 19 | $size: 28px; 20 | 21 | height: $size; 22 | width: $size; 23 | border-radius: 50%; 24 | border: 2px solid var(--checkbox-border-default); 25 | margin: 4px; 26 | position: relative; 27 | 28 | .radioButtonInner { 29 | position: absolute; 30 | height: calc($size / 2); 31 | width: calc($size / 2); 32 | top: 50%; 33 | left: 50%; 34 | transform: translate(-50%, -50%); 35 | border-radius: 50%; 36 | background: var(--interactive-normal); 37 | } 38 | } 39 | 40 | &:hover { 41 | background-color: var(--background-mod-subtle); 42 | } 43 | 44 | &.selected { 45 | color: var(--interactive-active); 46 | background-color: var(--background-mod-subtle); 47 | 48 | .radioButton { 49 | background-color: var(--background-brand); 50 | .radioButtonInner { 51 | background: #fff; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugins/dorion-titlebar/index.scss: -------------------------------------------------------------------------------- 1 | .dorion_topbar { 2 | background-color: var(--background-base-lowest); 3 | height: 32px; 4 | width: calc(100% - 8px); 5 | padding-left: 8px; 6 | position: relative; 7 | top: 0; 8 | left: 0; 9 | z-index: 9999; 10 | 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: flex-end; 14 | 15 | font-family: 'gg mono', 'Courier New', monospace; 16 | font-weight: bolder; 17 | 18 | color: var(--text-default); 19 | 20 | white-space: nowrap; 21 | } 22 | 23 | .topright { 24 | display: flex; 25 | flex-direction: row; 26 | align-items: center; 27 | margin-left: calc(-1 * var(--space-12)); 28 | height: 100%; 29 | } 30 | 31 | .topclose, 32 | .topmin, 33 | .topmax { 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | 38 | height: 100%; 39 | width: 44px; 40 | 41 | color: var(--interactive-text-default); 42 | 43 | transition: all 0.1s ease-in-out; 44 | } 45 | 46 | .topclose:hover, 47 | .topmin:hover, 48 | .topmax:hover { 49 | filter: brightness(0.8); 50 | background: var(--background-surface-highest); 51 | } 52 | 53 | .topmax svg { 54 | display: none; 55 | } 56 | 57 | .topclose svg, 58 | .topmin svg, 59 | .topmax svg { 60 | height: 10px !important; 61 | width: 10px !important; 62 | } 63 | 64 | .topmax:not(.maximized) .svgmax, 65 | .topmax.maximized .svgunmax { 66 | display: initial; 67 | } 68 | 69 | .topclose:hover { 70 | background: var(--status-danger); 71 | color: var(--white); 72 | } 73 | -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/util/events.ts: -------------------------------------------------------------------------------- 1 | import { event } from '../../../api/api.js' 2 | import { keybindActions } from './actionMap.js' 3 | 4 | const { 5 | flux: { 6 | dispatcher: FluxDispatcher 7 | } 8 | } = shelter 9 | 10 | const events = [] 11 | 12 | export const register = () => { 13 | events.push(event.listen('keybind_pressed', (e) => { 14 | const key = e.payload 15 | const action = keybindActions?.[key] 16 | 17 | if (!action || !action.press) return 18 | 19 | for (const press of action.press) { 20 | let e = press 21 | 22 | if (action.storeValue) { 23 | const { store, modify } = action.storeValue 24 | const storeInstance = shelter.flux.stores[store] 25 | e = modify(e, storeInstance) 26 | } 27 | 28 | FluxDispatcher.dispatch( 29 | e 30 | ) 31 | } 32 | })) 33 | 34 | events.push(event.listen('keybind_released', (e) => { 35 | const key = e.payload 36 | const action = keybindActions?.[key] 37 | 38 | if (!action || !action.release) return 39 | 40 | for (const release of action.release) { 41 | let e = release 42 | 43 | if (action.storeValue) { 44 | const { store, modify } = action.storeValue 45 | const storeInstance = shelter.flux.stores[store] 46 | e = modify(e, storeInstance) 47 | } 48 | 49 | FluxDispatcher.dispatch( 50 | release 51 | ) 52 | } 53 | 54 | })) 55 | } 56 | 57 | export const unregister = () => { 58 | events.forEach((e) => e()) 59 | } 60 | -------------------------------------------------------------------------------- /plugins/youre-right/index.tsx.scss: -------------------------------------------------------------------------------- 1 | .youreRightItem { 2 | transform: scaleX(-1); 3 | 4 | /* text within the container should be double-flipped */ 5 | video, 6 | h3, 7 | [id^=message-content-], 8 | [id^=username] { 9 | transform: scaleX(-1); 10 | text-align: right; 11 | margin: 0; 12 | } 13 | 14 | video { 15 | transform: scaleX(1); 16 | } 17 | 18 | img { 19 | text-align: right; 20 | margin: 0; 21 | } 22 | 23 | code { 24 | text-align: left; 25 | } 26 | 27 | img[class^=emoji] { 28 | transform: scaleX(1) !important; 29 | } 30 | 31 | span[class^=repliedTextPlaceholder] { 32 | transform: scaleX(-1); 33 | } 34 | 35 | [id^=message-content-] { 36 | max-width: 60%; 37 | text-align: right; 38 | } 39 | 40 | [class^=nonVisualMediaItemContainer], 41 | [class^=imageWrapper], 42 | [class^=avatar], 43 | [class^=embedWrapper], 44 | [class^=reaction_], 45 | [class^=buttonsInner] { 46 | transform: scaleX(-1); 47 | } 48 | 49 | [class*=timestampVisible] { 50 | transform: scaleX(-1); 51 | text-align: left !important; 52 | } 53 | 54 | [id^=message-reply-context-] { 55 | // /* revert */ 56 | // transform: scaleX(-1); 57 | 58 | /* bring to the right */ 59 | justify-content: flex-start; 60 | 61 | [class^=username] { 62 | transform: scaleX(-1); 63 | } 64 | } 65 | 66 | img:active { 67 | transform: scaleX(-1) translateY(-1px); 68 | } 69 | 70 | [role^=textbox] { 71 | transform: scaleX(-1); 72 | text-align: right; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /plugins/blur-nsfw/index.ts: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | dispatcher, 4 | stores: { 5 | ChannelStore 6 | } 7 | }, 8 | } = shelter 9 | 10 | let injectedCss = false 11 | let tempStyle = null 12 | 13 | const handleNsfwChannelSelect = async (payload) => { 14 | const { channelId } = payload 15 | const channel = ChannelStore?.getChannel(channelId) 16 | 17 | if (!channel.nsfw_) { 18 | if (tempStyle) { 19 | tempStyle.remove() 20 | injectedCss = false 21 | } 22 | 23 | return 24 | } 25 | 26 | if (injectedCss) return 27 | 28 | const style = document.createElement('style') 29 | style.innerText = ` 30 | div[class*="imageWrapper_"] video, 31 | div[class*="imageWrapper_"] img { 32 | filter: blur(10px) !important; 33 | 34 | transition: filter 0.5s ease; 35 | } 36 | 37 | /* On hover, show the image */ 38 | div[class*="imageWrapper_"]:hover video, 39 | div[class*="imageWrapper_"]:hover img { 40 | filter: blur(0) !important; 41 | } 42 | 43 | /* If the user clicked it, the don't need it blurred anymore */ 44 | div[class*="focusLock_"] video, 45 | div[class*="focusLock_"] img { 46 | filter: blur(0) !important; 47 | } 48 | ` 49 | 50 | tempStyle = document.body.appendChild(style) 51 | } 52 | 53 | dispatcher.subscribe('CHANNEL_SELECT', handleNsfwChannelSelect) 54 | 55 | export const onUnload = () => { 56 | dispatcher.unsubscribe('CHANNEL_SELECT', handleNsfwChannelSelect) 57 | 58 | if (tempStyle) { 59 | tempStyle.remove() 60 | injectedCss = false 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/components/Keybinds.tsx.scss: -------------------------------------------------------------------------------- 1 | .keybindSection { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: space-between; 6 | align-items: flex-start; 7 | 8 | margin-bottom: 20px; 9 | } 10 | 11 | .header { 12 | margin-bottom: 20px; 13 | } 14 | 15 | .keybindsHeader { 16 | width: 100%; 17 | height: 40px; 18 | 19 | margin-bottom: 20px; 20 | 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-between; 24 | align-items: center; 25 | } 26 | 27 | .keybindsBanner { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-around; 31 | align-items: flex-start; 32 | 33 | width: 75%; 34 | 35 | border-radius: 4px; 36 | 37 | border: 1px solid var(--status-warning) !important; 38 | background-color: var(--info-warning-background); 39 | padding: 12px; 40 | } 41 | 42 | .keybindsButton { 43 | width: 20% !important; 44 | height: 100%; 45 | background-color: var(--brand-500) !important; 46 | } 47 | 48 | .keybindsSwitch { 49 | width: 100%; 50 | } 51 | 52 | .keybindRestartCard { 53 | display: flex; 54 | flex-direction: column; 55 | justify-content: space-around; 56 | align-items: center; 57 | 58 | border: 1px solid var(--status-warning) !important; 59 | background-color: var(--info-warning-background); 60 | padding: 16px; 61 | 62 | width: 100%; 63 | margin-bottom: 20px; 64 | } 65 | 66 | .keybindRestartButton { 67 | width: 100% !important; 68 | background-color: var(--status-warning) !important; 69 | margin-top: 8px; 70 | } -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/api.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../../api/api.js' 2 | 3 | interface ThemeOptions { 4 | filter?: string 5 | page?: string 6 | sort?: 'popular' | 'creationdate' | 'name' | 'likes' | 'downloads' | 'recentlyupdated' 7 | } 8 | 9 | const BASE = 'https://betterdiscord.app' 10 | 11 | export const themeListEndpoint = async (options: ThemeOptions) => { 12 | const query = new URLSearchParams(options as Record) 13 | 14 | query.set('type', 'theme') 15 | query.set('pages', '1') 16 | query.set('sortDirection', 'descending') 17 | query.set('tags', '[]') 18 | 19 | const resp = await fetch(`${BASE}/Addon/GetApprovedAddons?${query}`) 20 | 21 | if (!resp.ok) { 22 | throw new Error('Failed to fetch themes') 23 | } 24 | 25 | const parser = new DOMParser() 26 | const dom = parser.parseFromString(await resp.text(), 'text/html') 27 | 28 | const themes = await Promise.all(Array.from(dom.querySelectorAll('.card-wrap')).map(async (e: Element) => ({ 29 | thumbnail: await api.util.fetchImage(`${BASE}${e.querySelector('.card-image')?.getAttribute('src')}`), 30 | name: e.querySelector('.card-title')?.textContent?.trim(), 31 | author: e.querySelector('.author-link')?.textContent?.trim(), 32 | description: e.querySelector('.card-description')?.textContent?.trim(), 33 | likes: e.querySelector('#addon-likes')?.textContent?.trim(), 34 | downloads: e.querySelector('#addon-downloads')?.textContent?.trim(), 35 | install_url: `${BASE}${e.querySelector('.btn-primary')?.getAttribute('href')}`, 36 | }))) 37 | 38 | return themes 39 | } -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/components/ThemePage.tsx.scss: -------------------------------------------------------------------------------- 1 | .shead { 2 | margin-top: 16px; 3 | margin-bottom: 8px; 4 | } 5 | 6 | .bot16 { 7 | margin-bottom: 16px; 8 | } 9 | 10 | .themeCards { 11 | display: grid; 12 | grid-template-columns: repeat(2, 1fr); 13 | grid-gap: 8px; 14 | margin-top: 16px; 15 | 16 | width: 100%; 17 | } 18 | 19 | .sortSection { 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: space-between; 23 | align-items: center; 24 | } 25 | 26 | .searchBox { 27 | flex-grow: 0 !important; 28 | width: 50%; 29 | } 30 | 31 | .pagesOuter { 32 | display: flex; 33 | flex-direction: row; 34 | justify-content: center; 35 | align-items: center; 36 | 37 | width: 100%; 38 | } 39 | 40 | .pages { 41 | display: flex; 42 | flex-direction: row; 43 | justify-content: space-between; 44 | align-items: center; 45 | 46 | width: 30%; 47 | 48 | margin-top: 16px; 49 | } 50 | 51 | .pageBtn { 52 | color: var(--text-default); 53 | cursor: pointer; 54 | } 55 | 56 | .pageBtn:hover { 57 | text-decoration: underline; 58 | } 59 | 60 | input[type=number] { 61 | width: 50px; 62 | text-align: center; 63 | margin: 0 8px; 64 | padding: 4px; 65 | 66 | font-size: 16px; 67 | 68 | background-color: var(--background-base-lowest); 69 | color: var(--text-default); 70 | border: none; 71 | border-radius: 4px; 72 | } 73 | 74 | input[type=number] { 75 | -moz-appearance: textfield; 76 | appearance: textfield; 77 | } 78 | 79 | input::-webkit-outer-spin-button, 80 | input::-webkit-inner-spin-button { 81 | -webkit-appearance: none; 82 | } -------------------------------------------------------------------------------- /api/dorion.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Dorion', 3 | 4 | invoke: (name: string, payload?: any) => { 5 | // for tauri v2, `invoke` exists on __TAURI__.core, instead of base __TAURI__, so we should check for both 6 | if ((window as any).__TAURI__?.invoke) { 7 | return (window as any).__TAURI__.invoke(name, payload) 8 | } else { 9 | return (window as any).__TAURI__.core.invoke(name, payload) 10 | } 11 | }, 12 | event: { 13 | emit: (name: string, payload: any) => { 14 | return (window as any).__TAURI__.event.emit(name, payload) 15 | }, 16 | listen: async (name: string, callback: (payload: any) => void) => { 17 | return (window as any).__TAURI__.event.listen(name, callback) 18 | } 19 | }, 20 | app: { 21 | getVersion: () => { 22 | return (window as any).__TAURI__.app.getVersion() 23 | } 24 | }, 25 | process: { 26 | relaunch: () => { 27 | return (window as any).__TAURI__.process.relaunch() 28 | } 29 | }, 30 | apiWindow: { 31 | appWindow: { 32 | setFullscreen: (isFullscreen: boolean) => { 33 | // for tauri v2, `setFullscreen` exists on __TAURI__.webviewWindow.getCurrent()[0], instead of base __TAURI__.window.appWindow, so we should check for both 34 | if ((window as any).__TAURI__?.webviewWindow?.getCurrentWebviewWindow) { 35 | return (window as any).__TAURI__.webviewWindow.getCurrentWebviewWindow().setFullscreen(isFullscreen) 36 | } else { 37 | return (window as any).__TAURI__.window.appWindow.setFullscreen(isFullscreen) 38 | } 39 | } 40 | } 41 | } 42 | } satisfies Backend -------------------------------------------------------------------------------- /plugins/youre-right/index.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './index.tsx.scss' 2 | 3 | const { 4 | flux: { 5 | storesFlat: { 6 | UserStore, 7 | SelectedChannelStore 8 | }, 9 | }, 10 | util: { 11 | getFiber, 12 | reactFiberWalker 13 | }, 14 | plugin: { 15 | scoped: { 16 | flux: { 17 | subscribe 18 | } 19 | } 20 | }, 21 | observeDom 22 | } = shelter 23 | 24 | const style = document.createElement('style') 25 | style.innerHTML = css 26 | style.id = 'youre-right-styles' 27 | 28 | document.head.appendChild(style) 29 | 30 | function handleElm(elm) { 31 | const message = reactFiberWalker(getFiber(elm), 'message', true)?.pendingProps?.message 32 | const id = UserStore.getCurrentUser().id 33 | if (!message || message.author.id !== id || elm.classList.contains(classes.youreRightItem)) return 34 | 35 | elm.classList.add(classes.youreRightItem) 36 | } 37 | 38 | function handleDispatch(payload) { 39 | // only listen for message_create in the current channel 40 | if (payload.type === 'MESSAGE_CREATE' && payload.channelId !== SelectedChannelStore.getChannelId()) 41 | return 42 | 43 | const unObserve = observeDom('li[id^=chat-messages-]', (elem) => { 44 | handleElm(elem) 45 | unObserve() 46 | }) 47 | 48 | setTimeout(unObserve, 500) 49 | } 50 | 51 | const triggers = ['MESSAGE_CREATE', 'CHANNEL_SELECT', 'LOAD_MESSAGES_SUCCESS', 'UPDATE_CHANNEL_DIMENSIONS'] 52 | for (const t of triggers) 53 | subscribe(t, handleDispatch) 54 | 55 | export const onUnload = () => { 56 | style.remove() 57 | for (const t of triggers) 58 | subscribe(t, handleDispatch) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /plugins/platform-spoof/index.tsx: -------------------------------------------------------------------------------- 1 | import { createApi, webpackChunk } from '@cumjar/websmack' 2 | import { after } from 'spitroast' 3 | import { RadioGroup } from '../../components/RadioGroup' 4 | 5 | const { 6 | plugin: { 7 | store 8 | }, 9 | ui: { 10 | Header, 11 | HeaderTags 12 | } 13 | } = shelter 14 | 15 | const chunk = webpackChunk() 16 | const wp = chunk && createApi([undefined, ...chunk]) 17 | const s = wp.findByProps('getSuperProperties') 18 | 19 | if (!s) { 20 | throw new Error('Failed to find identification function') 21 | } 22 | 23 | // @ts-expect-error defining to global window 24 | window.PlatformSpoof = { 25 | desktop: 'Discord Client', 26 | web: 'Chrome', 27 | mobile: 'Android', 28 | 29 | setSpoof: (type: 'desktop' | 'web' | 'mobile') => { 30 | store.clientType = type 31 | }, 32 | } 33 | 34 | after('getSuperProperties', s, (args, response) => { 35 | return { 36 | ...response, 37 | // @ts-expect-error spoofing the client type 38 | ...{browser: window.PlatformSpoof?.[store.clientType] ?? window.PlatformSpoof.desktop}, 39 | } 40 | }) 41 | 42 | export const settings = () => ( 43 | <> 44 | Client Type 45 | 46 | (store.clientType = v)} 63 | /> 64 | > 65 | ) 66 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | # Shamelessly yoinked from https://github.com/ioj4/shelter-plugins/blob/master/.github/workflows/deploy.yml 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: "18" 22 | 23 | - name: Deps 24 | run: | 25 | npm install --global pnpm 26 | pnpm i 27 | 28 | - name: Build plugin(s) 29 | run: pnpm lune ci 30 | 31 | - name: Copy additional files 32 | run: | 33 | cp README.md dist/README.md 34 | printf -- "---\npermalink: /404.html\n---\n" > dist/404.md 35 | printf -- ">You are somewhere you probably shouldn't be. If you meant to install a plugin, copy the link into Shelter\n\n" >> dist/404.md 36 | printf -- "remote_theme: dracula/gh-pages\n" > dist/_config.yml 37 | cat README.md >> dist/404.md 38 | 39 | # Documentation: https://github.com/peaceiris/actions-gh-pages 40 | - name: Deploy to GitHub Pages 41 | uses: peaceiris/actions-gh-pages@v3 42 | with: 43 | github_token: ${{ secrets.GITHUB_TOKEN }} 44 | publish_dir: ./dist 45 | # Makes it so the md files in the previous step get processed by GitHub Pages 46 | enable_jekyll: true 47 | -------------------------------------------------------------------------------- /plugins/shelteRPC/components/GameCard.scss: -------------------------------------------------------------------------------- 1 | .gameCard { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: space-between; 6 | 7 | width: 100%; 8 | height: 72px; 9 | 10 | border-radius: 5px; 11 | 12 | color: var(--text-default); 13 | 14 | margin: 12px 0; 15 | 16 | &.cardNone { 17 | background-color: var(--background-surface-highest); 18 | } 19 | 20 | &.cardPlaying { 21 | background-color: var(--status-positive-background); 22 | 23 | .gameCardIcons { 24 | color: var(--green-230); 25 | } 26 | } 27 | 28 | &.cardPlayed { 29 | background: transparent; 30 | border-radius: 0; 31 | border-bottom: 1px solid var(--primary-500); 32 | 33 | .gameCardLastPlayed { 34 | color: var(--text-muted); 35 | } 36 | 37 | .lastPlayedTimestamp { 38 | font-weight: bold; 39 | } 40 | } 41 | } 42 | 43 | .gameCardInfo { 44 | display: flex; 45 | flex-direction: column; 46 | align-items: flex-start; 47 | justify-content: center; 48 | 49 | width: 70%; 50 | height: 100%; 51 | 52 | padding: 0 20px; 53 | } 54 | 55 | .gameCardName { 56 | font-weight: bold; 57 | } 58 | 59 | .gameCardIcons { 60 | display: flex; 61 | flex-direction: row; 62 | align-items: center; 63 | justify-content: flex-end; 64 | 65 | height: 100%; 66 | 67 | padding: 0 20px; 68 | 69 | color: var(--primary-400); 70 | 71 | & span { 72 | margin: 4px; 73 | 74 | width: 24px; 75 | height: 24px; 76 | 77 | cursor: pointer; 78 | 79 | &:hover { 80 | color: var(--text-default); 81 | } 82 | 83 | & svg { 84 | width: 100%; 85 | height: 100%; 86 | } 87 | } 88 | } 89 | 90 | .trash:hover { 91 | color: var(--status-danger) !important; 92 | } 93 | -------------------------------------------------------------------------------- /components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './Dropdown.tsx.scss' 2 | import { SelectArrow } from './SelectArrow' 3 | 4 | const { 5 | ui: { injectCss }, 6 | } = shelter 7 | 8 | let injectedCss = false 9 | 10 | export const Dropdown: Component<{ 11 | value?: string; 12 | placeholder?: string; 13 | id?: string; 14 | 'aria-label'?: string; 15 | onChange?(e): void; 16 | style?: any 17 | options?: { 18 | label: string; 19 | value: string; 20 | }[]; 21 | disabled?: boolean; 22 | immutable?: boolean; 23 | }> = (props) => { 24 | if (!injectedCss) { 25 | injectedCss = true 26 | injectCss(css) 27 | } 28 | 29 | return ( 30 | 31 | { 39 | props.onChange(e) 40 | 41 | if (props.immutable) { 42 | e.preventDefault() 43 | e.stopPropagation() 44 | e.target.value = props.value 45 | } 46 | }} 47 | disabled={props.disabled} 48 | > 49 | {props.placeholder && ( 50 | 51 | {props.placeholder} 52 | 53 | )} 54 | 55 | {props.options?.map((o) => ( 56 | 57 | {o.label} 58 | 59 | ))} 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /plugins/userpfp/index.tsx: -------------------------------------------------------------------------------- 1 | import { createApi, webpackChunk } from '@cumjar/websmack' 2 | import { after } from 'spitroast' 3 | import { css, classes } from './index.scss' 4 | 5 | const { 6 | ui: { 7 | SwitchItem, 8 | LinkButton, 9 | injectCss, 10 | }, 11 | plugin: { 12 | store 13 | } 14 | } = shelter 15 | 16 | const DATA_URL = 'https://userpfp.github.io/UserPFP/source/data.json' 17 | 18 | const chunk = webpackChunk() 19 | const wp = chunk && createApi([undefined, ...chunk]) 20 | const c = wp.findByPropsAll('getUserAvatarURL') 21 | 22 | for (const m of c) { 23 | after('getUserAvatarURL', m, (args, response) => { 24 | return store.preferNitro && response.includes('a_') ? response : window.userpfp.getUrl(args[0]) ?? response 25 | }) 26 | } 27 | 28 | declare global { 29 | interface Window { 30 | userpfp: { 31 | avatars: Record 32 | getUrl: (id: string) => string 33 | } 34 | } 35 | } 36 | 37 | let injectedCss = false 38 | 39 | if (!injectedCss) { 40 | injectedCss = true 41 | injectCss(css) 42 | } 43 | 44 | export const settings = () => ( 45 | <> 46 | 50 | Submit your PFP here! 51 | 52 | 53 | (store.preferNitro = v)} 56 | tooltip="If the user has Nitro but also has a custom UserPFP, prefer the Nitro one." 57 | > 58 | Prefer Nitro 59 | 60 | > 61 | ) 62 | 63 | export const onLoad = async () => { 64 | const resp = await fetch(DATA_URL) 65 | window.userpfp = await resp.json() 66 | window.userpfp.getUrl = (id: string) => window.userpfp.avatars[id] ?? null 67 | } -------------------------------------------------------------------------------- /api/api.ts: -------------------------------------------------------------------------------- 1 | import dorionBackend from './dorion.js' 2 | import flooedBackend from './flooed.js' 3 | import noneBackend from './none.js' 4 | 5 | type BackendName = 'Dorion' | 'Flooed' | 'None' 6 | 7 | interface GlobalApi { 8 | util: { 9 | cssSanitize: (css: string) => string 10 | fetchImage: (url: string) => Promise 11 | applyNotificationCount: () => void 12 | waitForElm: (selector: string) => Promise 13 | } 14 | 15 | shouldShowUnreadBadge: boolean 16 | } 17 | 18 | declare global { 19 | interface Window { 20 | Dorion?: GlobalApi 21 | Flooed?: GlobalApi & { 22 | invoke: (name: string, payload?: any) => Promise 23 | version: string 24 | name: string 25 | } 26 | } 27 | } 28 | 29 | let backendName: BackendName = 'None' 30 | 31 | // Determine backend to use 32 | if (window.Dorion) { 33 | backendName = 'Dorion' 34 | } else if (window.Flooed) { 35 | backendName = 'Flooed' 36 | } 37 | 38 | // As much as I hate doing a runtime check (bundle size go brazy), it's the only real way to make this work 39 | let backendObj 40 | 41 | switch (backendName) { 42 | case 'Dorion': 43 | backendObj = dorionBackend 44 | break 45 | case 'Flooed': 46 | backendObj = flooedBackend 47 | break 48 | default: 49 | backendObj = noneBackend 50 | break 51 | } 52 | 53 | export const backend = backendName 54 | export const api = window[backendName] as GlobalApi 55 | 56 | export let requiresRestart = false 57 | export const backendRestartRequired = (v: boolean) => { 58 | requiresRestart = v 59 | } 60 | 61 | export const appName = backendObj.name 62 | export const invoke = backendObj.invoke 63 | export const event = backendObj.event 64 | export const app = backendObj.app 65 | export const process = backendObj.process 66 | export const apiWindow = backendObj.apiWindow 67 | -------------------------------------------------------------------------------- /util/theme.ts: -------------------------------------------------------------------------------- 1 | import { appName, api, invoke } from '../api/api.js' 2 | 3 | export const installAndLoad = async (link: string, statusUpdater: (string) => void, filename?: string) => { 4 | statusUpdater('Fetching...') 5 | 6 | const themeName = await invoke('theme_from_link', { 7 | link, 8 | filename 9 | }) 10 | 11 | statusUpdater(`Applying ${themeName} ...`) 12 | 13 | // Save the theme to the config 14 | const config = JSON.parse(await invoke('read_config_file')) 15 | 16 | config?.themes?.push(themeName) 17 | 18 | statusUpdater('Saving...') 19 | 20 | await invoke('write_config_file', { 21 | contents: JSON.stringify(config) 22 | }) 23 | 24 | // Get the Dorion theme style tag, replace the contents 25 | await reloadThemes() 26 | 27 | statusUpdater('Done!') 28 | 29 | return themeName 30 | } 31 | 32 | 33 | export const reloadThemes = async () => { 34 | // Get the Dorion theme style tag, replace the contents 35 | const themeTag = document.getElementById(`${appName.toLowerCase()}-theme`) as HTMLStyleElement 36 | 37 | // Get the initial theme 38 | const themeContents = await invoke('get_themes').catch(e => console.error(e)) 39 | 40 | // Create a "name" for the "theme" (or combo) based on the retrieved enabled theme list 41 | const themeNames = await invoke('get_enabled_themes').catch(e => console.error(e)) || [] 42 | // Gotta adhere to filename length restrictions 43 | const themeName = themeNames.join('').substring(0, 254) 44 | 45 | if (themeName === '') { 46 | themeTag.innerHTML = '' 47 | return 48 | } 49 | 50 | const localized = await invoke('localize_imports', { 51 | css: themeContents, 52 | name: themeName 53 | }) 54 | 55 | // Internal Dorion function 56 | const contents = api.util.cssSanitize(localized) 57 | 58 | themeTag.innerHTML = contents 59 | } 60 | -------------------------------------------------------------------------------- /components/KeybindInput.tsx.scss: -------------------------------------------------------------------------------- 1 | .keybindContainer { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: space-between; 6 | 7 | width: 100%; 8 | height: 40px; 9 | 10 | border-radius: 4px; 11 | 12 | background: var(--background-base-lowest); 13 | color: var(--text-default); 14 | border: 1px solid transparent; 15 | 16 | padding: 4px; 17 | 18 | transition: all 0.2s; 19 | } 20 | 21 | .keybindContainer:hover { 22 | border: 1px solid var(--status-danger); 23 | } 24 | 25 | .recording { 26 | border: 1px solid var(--status-danger); 27 | 28 | animation: pulse 1s infinite; 29 | 30 | .keybindButton { 31 | background: hsl(var(--red-400-hsl)/.1); 32 | color: var(--status-danger); 33 | } 34 | 35 | .keybindButton:hover { 36 | background: hsl(var(--red-400-hsl)/.2); 37 | } 38 | } 39 | 40 | .keybindInput { 41 | background: transparent; 42 | 43 | display: flex; 44 | align-items: center; 45 | } 46 | 47 | .keybindPlaceholder { 48 | color: var(--text-muted) !important; 49 | } 50 | 51 | .keybindButton { 52 | height: 30px; 53 | width: 50%; 54 | 55 | margin: 0; 56 | padding: 4px; 57 | 58 | border-radius: 4px; 59 | 60 | display: flex; 61 | align-items: center; 62 | 63 | background: var(--control-secondary-background-default); 64 | color: var(--white-500); 65 | 66 | border: 1px solid transparent; 67 | 68 | cursor: pointer; 69 | 70 | transition: all 0.2s; 71 | } 72 | 73 | .keybindButton:hover { 74 | background: var(--control-secondary-background-hover); 75 | } 76 | 77 | /* animation to pulse shadow intensity */ 78 | @keyframes pulse { 79 | 0% { 80 | box-shadow: 0 0 10px 0px hsl(var(--red-400-hsl)/.5); 81 | } 82 | 50% { 83 | box-shadow: 0 0 10px 4px hsl(var(--red-400-hsl)/.5); 84 | } 85 | 100% { 86 | box-shadow: 0 0 10px 0px hsl(var(--red-400-hsl)/.5); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /plugins/plugin-browser/components/PluginCard.tsx: -------------------------------------------------------------------------------- 1 | import { getPluginJson } from '../github.js' 2 | import { css, classes } from './PluginCard.scss' 3 | 4 | interface Props { 5 | plugin: string 6 | site: string 7 | author: string 8 | install_url: string 9 | } 10 | 11 | const { 12 | ui: { 13 | injectCss, 14 | Button, 15 | Text 16 | }, 17 | solid: { 18 | createSignal, 19 | createEffect, 20 | }, 21 | plugins: { 22 | installedPlugins, 23 | addRemotePlugin 24 | } 25 | } = shelter 26 | 27 | let injectedCss = false 28 | 29 | export function PluginCard(props: Props) { 30 | if (!injectedCss) { 31 | injectCss(css) 32 | injectedCss = true 33 | } 34 | 35 | const [info, setInfo] = createSignal({}) 36 | const [installed, setInstalled] = createSignal(false) 37 | 38 | createEffect(async () => { 39 | setInfo(await getPluginJson(props.site, props.plugin)) 40 | 41 | const installed = Object.values(installedPlugins?.() || {}).some((p: any) => p.manifest.name === info()?.name && p.manifest.author === info()?.author) 42 | setInstalled(installed) 43 | }) 44 | 45 | const installPlugin = () => { 46 | addRemotePlugin(props.plugin, props.install_url, true) 47 | setInstalled(true) 48 | } 49 | 50 | return ( 51 | 52 | 53 | {info()?.name || props.plugin} by {props.author} 54 | 55 | 56 | {info()?.description} 57 | 58 | 59 | 64 | { 65 | installed() ? 'Installed' : 'Install' 66 | } 67 | 68 | 69 | 70 | ) 71 | } -------------------------------------------------------------------------------- /plugins/inline-css/components/Window.scss: -------------------------------------------------------------------------------- 1 | .window { 2 | position: absolute; 3 | z-index: 99999; 4 | min-width: 300px; 5 | min-height: 300px; 6 | pointer-events: none; 7 | 8 | /* Start in middle */ 9 | top: 50%; 10 | left: 50%; 11 | } 12 | 13 | .resize { 14 | cursor: se-resize; 15 | position: absolute; 16 | bottom: 0; 17 | right: 0; 18 | width: 16px; 19 | height: 16px; 20 | z-index: -1; 21 | pointer-events: all; 22 | } 23 | 24 | .content { 25 | --inset: 8px; 26 | 27 | width: calc(100% - 2px - 2 * var(--inset)); 28 | height: calc(100% - 2px - 2 * var(--inset)); 29 | margin: var(--inset); 30 | pointer-events: all; 31 | 32 | background: var(--background-base-low); 33 | border-radius: 6px; 34 | border: 1px solid var(--background-surface-highest); 35 | 36 | box-shadow: 0 0 3px 0 rgba(0, 0, 0); 37 | 38 | & .inner { 39 | margin: 8px; 40 | overflow-y: auto; 41 | height: calc(100% - 50px); 42 | 43 | /* Scrollbar */ 44 | &::-webkit-scrollbar-corner { 45 | background: transparent 46 | } 47 | 48 | &::-webkit-scrollbar { 49 | background: transparent; 50 | } 51 | 52 | &::-webkit-scrollbar-track { 53 | background: none; 54 | } 55 | 56 | &::-webkit-scrollbar-thumb { 57 | background: var(--background-base-lowest); 58 | border-radius: 4px; 59 | } 60 | 61 | &::-webkit-scrollbar:horizontal { 62 | height: 8px; 63 | } 64 | 65 | &::-webkit-scrollbar:vertical { 66 | width: 8px; 67 | } 68 | } 69 | } 70 | 71 | .topbar { 72 | height: 30px; 73 | width: 100%; 74 | margin-bottom: 12px; 75 | background: var(--background-base-lowest); 76 | } 77 | 78 | .exit { 79 | height: 100%; 80 | width: 30px; 81 | 82 | display: flex; 83 | align-items: center; 84 | justify-content: center; 85 | 86 | &:hover { 87 | cursor: pointer; 88 | background: #f23f43; 89 | } 90 | 91 | & svg { 92 | height: 60%; 93 | width: 100%; 94 | } 95 | } 96 | 97 | .main { 98 | margin-right: 10px; 99 | } -------------------------------------------------------------------------------- /plugins/inline-css/components/Close.tsx: -------------------------------------------------------------------------------- 1 | export const Close = () => ( 2 | 11 | 12 | 26 | 42 | 58 | 59 | 60 | ) 61 | -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/PluginsPage.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from '../../../api/api.js' 2 | import { ClientModList } from '../components/ClientModList.jsx' 3 | import { PluginList } from '../components/PluginList.jsx' 4 | import { css, classes } from './PluginsPage.tsx.scss' 5 | import { WarningCard } from '../components/WarningCard.jsx' 6 | 7 | const { 8 | ui: { Header, Button, HeaderTags, injectCss, ButtonSizes}, 9 | solid: { createSignal }, 10 | } = shelter 11 | 12 | let injectedCss = false 13 | 14 | export function PluginsPage() { 15 | const [restartRequired , setRestartRequired] = createSignal(false) 16 | 17 | if (!injectedCss) { 18 | injectedCss = true 19 | injectCss(css) 20 | } 21 | 22 | const openPluginsFolder = () => { 23 | invoke('open_plugins') 24 | } 25 | 26 | const openExtensionsFolder = () => { 27 | invoke('open_extensions') 28 | } 29 | 30 | return ( 31 | <> 32 | Client Mods & Plugins 33 | 34 | {restartRequired() && ( 35 | 36 | )} 37 | 38 | Client Mods 39 | 40 | { 42 | setRestartRequired(true) 43 | }} 44 | /> 45 | 46 | Plugins 47 | 48 | { 50 | setRestartRequired(true) 51 | }} 52 | /> 53 | 54 | 55 | 62 | Open Plugins Folder 63 | 64 | 65 | 72 | Open Extensions Folder 73 | 74 | 75 | > 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /plugins/dorion-settings/types/release.ts: -------------------------------------------------------------------------------- 1 | export type TReleases = IRelease[] 2 | 3 | export interface IRelease { 4 | url: string 5 | assets_url: string 6 | upload_url: string 7 | html_url: string 8 | id: number 9 | author: IAuthor 10 | node_id: string 11 | tag_name: string 12 | target_commitish: string 13 | name: string 14 | draft: boolean 15 | prerelease: boolean 16 | created_at: string 17 | published_at: string 18 | assets: IAsset[] 19 | tarball_url: string 20 | zipball_url: string 21 | body: string 22 | reactions?: IReactions 23 | mentions_count?: number 24 | } 25 | 26 | export interface IAuthor { 27 | login: string 28 | id: number 29 | node_id: string 30 | avatar_url: string 31 | gravatar_id: string 32 | url: string 33 | html_url: string 34 | followers_url: string 35 | following_url: string 36 | gists_url: string 37 | starred_url: string 38 | subscriptions_url: string 39 | organizations_url: string 40 | repos_url: string 41 | events_url: string 42 | received_events_url: string 43 | type: string 44 | site_admin: boolean 45 | } 46 | 47 | export interface IAsset { 48 | url: string 49 | id: number 50 | node_id: string 51 | name: string 52 | label: any 53 | uploader: IUploader 54 | content_type: string 55 | state: string 56 | size: number 57 | download_count: number 58 | created_at: string 59 | updated_at: string 60 | browser_download_url: string 61 | } 62 | 63 | export interface IUploader { 64 | login: string 65 | id: number 66 | node_id: string 67 | avatar_url: string 68 | gravatar_id: string 69 | url: string 70 | html_url: string 71 | followers_url: string 72 | following_url: string 73 | gists_url: string 74 | starred_url: string 75 | subscriptions_url: string 76 | organizations_url: string 77 | repos_url: string 78 | events_url: string 79 | received_events_url: string 80 | type: string 81 | site_admin: boolean 82 | } 83 | 84 | export interface IReactions { 85 | url: string 86 | total_count: number 87 | '+1': number 88 | '-1': number 89 | laugh: number 90 | hooray: number 91 | confused: number 92 | heart: number 93 | rocket: number 94 | eyes: number 95 | } 96 | -------------------------------------------------------------------------------- /plugins/dorion-tray/index.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '../../api/api.js' 2 | 3 | const { 4 | flux: { 5 | dispatcher, 6 | }, 7 | } = shelter 8 | 9 | const state = { 10 | video: false, 11 | streaming: false, 12 | deafened: false, 13 | muted: false, 14 | speaking: false, 15 | connected: false, 16 | } 17 | 18 | const handleConnect = async (payload) => { 19 | const {state: connectionState} = payload 20 | 21 | if (connectionState.toLowerCase() === 'connected' || connectionState.toLowerCase() === 'connecting') { 22 | state.connected = true 23 | } else if (connectionState.toLowerCase() === 'disconnected') { 24 | state.connected = false 25 | } 26 | 27 | await handleTrayUpdate() 28 | } 29 | 30 | const handleVoiceChannelActions = async (payload) => { 31 | const loggedInUserId = localStorage.getItem('user_id_cache').replace(/"/g, '') 32 | const voiceState = payload.voiceStates.find((voiceState) => voiceState.userId === loggedInUserId) 33 | 34 | if (!voiceState) return 35 | 36 | const {selfDeaf, selfMute, selfStream, selfVideo} = voiceState 37 | 38 | state.muted = selfMute 39 | state.deafened = selfDeaf 40 | state.streaming = selfStream 41 | state.video = selfVideo 42 | 43 | await handleTrayUpdate() 44 | } 45 | 46 | const handleSpeakAction = async (payload) => { 47 | const loggedInUserId = localStorage.getItem('user_id_cache').replace(/"/g, '') 48 | const {userId, speakingFlags } = payload 49 | 50 | if (userId !== loggedInUserId) return 51 | 52 | state.speaking = speakingFlags > 0 53 | 54 | await handleTrayUpdate() 55 | } 56 | 57 | const handleTrayUpdate = async () => { 58 | const icon = (!state.connected && 'disconnected') || Object.keys(state).find(k => state[k]) 59 | await invoke('set_tray_icon', { event: icon }) 60 | } 61 | 62 | dispatcher.subscribe('VOICE_STATE_UPDATES', handleVoiceChannelActions) 63 | dispatcher.subscribe('SPEAKING', handleSpeakAction) 64 | dispatcher.subscribe('RTC_CONNECTION_STATE', handleConnect) 65 | 66 | export const onUnload = () => { 67 | dispatcher.unsubscribe('VOICE_STATE_UPDATES', handleVoiceChannelActions) 68 | dispatcher.unsubscribe('SPEAKING', handleSpeakAction) 69 | dispatcher.unsubscribe('RTC_CONNECTION_STATE', handleConnect) 70 | } 71 | -------------------------------------------------------------------------------- /plugins/inline-css/components/Popout.tsx: -------------------------------------------------------------------------------- 1 | export const Popout = () => ( 2 | 11 | 12 | 16 | 22 | 28 | 34 | 35 | 36 | ) -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/index.tsx: -------------------------------------------------------------------------------- 1 | import { Keybinds } from './components/Keybinds' 2 | import { register, unregister } from './util/events' 3 | 4 | const { 5 | flux: { 6 | dispatcher: FluxDispatcher, 7 | }, 8 | observeDom, 9 | ui: { 10 | ReactiveRoot 11 | } 12 | } = shelter 13 | 14 | let child: Element = null 15 | 16 | const viewedKeybindsCallback = (e) => { 17 | if (e.section !== 'Keybinds') { 18 | if (child) { 19 | child.remove() 20 | child = null 21 | } 22 | 23 | return 24 | } 25 | 26 | const unsub = observeDom('#keybinds-tab', () => { 27 | unsub() 28 | 29 | const oldElm = document.querySelector('div[class*="-browserNotice"') 30 | const owner = shelter.util.getFiberOwner(oldElm) 31 | const keybindsArea = oldElm.parentElement 32 | 33 | // hide browser notice 34 | // @ts-expect-error this is real 35 | oldElm.style.display = 'none' 36 | 37 | // Find the divider in the keybinds area 38 | const divider = keybindsArea.parentElement.parentElement.querySelector('div[class*="-divider"]') 39 | if (divider) 40 | // @ts-expect-error this is real 41 | divider.style.display = 'none' 42 | 43 | // Remove big margin on the default keybinds bit 44 | const defaultKeybinds = keybindsArea.parentElement.parentElement.querySelector('div[class*="marginTop"]') 45 | if (defaultKeybinds) 46 | // @ts-expect-error this is real 47 | defaultKeybinds.style.marginTop = '0' 48 | 49 | child = keybindsArea.appendChild( 50 | 51 | k.value !== 'PUSH_TO_TALK')} 56 | // @ts-expect-error it does exist I promise 57 | keybindDescriptions={owner.keybindDescriptions} 58 | /> 59 | 60 | ) 61 | }) 62 | } 63 | 64 | const subscriptions = [ 65 | FluxDispatcher.subscribe('USER_SETTINGS_MODAL_SET_SECTION', viewedKeybindsCallback) 66 | ] 67 | 68 | register() 69 | 70 | export const onUnload = () => { 71 | for (const unsub of subscriptions) { 72 | unsub() 73 | } 74 | 75 | unregister() 76 | } 77 | -------------------------------------------------------------------------------- /plugins/openasar-dom-optimizer/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * OpenAsar has this cool little optimization technique that delays some operations when 3 | * switching channels or servers, so that the switch is faster. 4 | * 5 | * https://github.com/GooseMod/OpenAsar/blob/ef4470849624032a8eb7265eabd23158aa5a2356/src/mainWindow.js#L99 6 | * https://github.com/GooseMod/OpenAsar/wiki/DOM-Optimizer 7 | */ 8 | 9 | const { 10 | plugin: { 11 | store 12 | }, 13 | ui: { 14 | SwitchItem, 15 | Text 16 | } 17 | } = shelter 18 | 19 | const _removeChild = Element.prototype.removeChild 20 | //const _appendChild = Element.prototype.appendChild 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 23 | const optimize = (orig: Function) => 24 | function (...args: unknown[]) { 25 | if ( 26 | // @ts-expect-error womp womp 27 | typeof args[0].className === 'string' && 28 | // @ts-expect-error womp womp 29 | args[0].className.indexOf('activity') !== -1 30 | ) 31 | return setTimeout(() => orig.apply(this, args), 100) 32 | 33 | return orig.apply(this, args) 34 | } 35 | 36 | if (store.remove) { 37 | Element.prototype.removeChild = optimize(Element.prototype.removeChild) 38 | } 39 | 40 | // if (store.append) { 41 | // Element.prototype.appendChild = optimize(Element.prototype.appendChild) 42 | // } 43 | 44 | export const settings = () => ( 45 | <> 46 | 47 | See the OpenAsar wiki for more information on how this works! 48 | 49 | 50 | 51 | {/* 52 | { 55 | store.append = v 56 | if (v) { 57 | Element.prototype.appendChild = optimize(_appendChild) 58 | } else { 59 | Element.prototype.appendChild = _appendChild 60 | } 61 | }} 62 | > 63 | Apply to Element.appendChild 64 | 65 | */} 66 | 67 | { 70 | store.remove = v 71 | if (v) { 72 | Element.prototype.removeChild = optimize(_removeChild) 73 | } else { 74 | Element.prototype.removeChild = _removeChild 75 | } 76 | }} 77 | > 78 | Apply to Element.removeChild 79 | 80 | > 81 | ) 82 | -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/components/ThemeCard.tsx: -------------------------------------------------------------------------------- 1 | import { installAndLoad } from '../../../util/theme.js' 2 | import { basicModal } from '../../../util/modal.jsx' 3 | 4 | import { css, classes } from './ThemeCard.tsx.scss' 5 | 6 | interface Props { 7 | key: string 8 | theme: string 9 | thumbnail: string 10 | likes: string 11 | downloads: string 12 | description: string 13 | author: string 14 | install_url: string 15 | } 16 | 17 | const { 18 | ui: { 19 | injectCss, 20 | Button, 21 | Text, 22 | openModal, 23 | }, 24 | solid: { 25 | createSignal, 26 | createEffect, 27 | } 28 | } = shelter 29 | 30 | let injectedCss = false 31 | 32 | export function ThemeCard(props: Props) { 33 | if (!injectedCss) { 34 | injectCss(css) 35 | injectedCss = true 36 | } 37 | 38 | console.log(props) 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | {props.theme} by {props.author} 47 | 48 | 49 | {props.description} 50 | 51 | 52 | themeInstallationModel(props.install_url, props.theme)} 55 | > 56 | Install 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | export const themeInstallationModel = async (link: string, name: string) => { 65 | const [status, setStatus] = createSignal('') 66 | const [closeFn, setCloseFn] = createSignal<() => void>(() => {}) 67 | 68 | createEffect(async () => { 69 | await installAndLoad(link, (s) => { 70 | setStatus(s) 71 | console.log(s) 72 | }, name).catch(e => { 73 | setStatus(e) 74 | }) 75 | 76 | closeFn() 77 | }) 78 | 79 | openModal((props) => { 80 | setCloseFn(props.close) 81 | 82 | return basicModal({ 83 | header: 'Install Theme', 84 | body: ( 85 | 86 | 92 | {status()} 93 | 94 | 95 | ), 96 | }) 97 | }) 98 | } -------------------------------------------------------------------------------- /plugins/plugin-browser/storage.ts: -------------------------------------------------------------------------------- 1 | export function createLocalStorage() { 2 | const iframe = document.createElement('iframe') 3 | 4 | // Wait for document.head to exist, then append the iframe 5 | const interval = setInterval(() => { 6 | if (!document.head) return 7 | 8 | document.head.append(iframe) 9 | const pd = Object.getOwnPropertyDescriptor(iframe.contentWindow, 'localStorage') 10 | iframe.remove() 11 | 12 | if (!pd) return 13 | 14 | Object.defineProperty(window, 'localStorage', pd) 15 | 16 | clearInterval(interval) 17 | }, 50) 18 | } 19 | 20 | // Save the list of all plugins and repos to localStorage 21 | export function savePluginsCache(cache: object) { 22 | localStorage.setItem('plugins-browser-cache', `${Date.now()};${JSON.stringify(cache)}`) 23 | } 24 | 25 | export function getPluginsCache() { 26 | const cache = localStorage.getItem('plugins-browser-cache') 27 | if (!cache) { 28 | return null 29 | } 30 | 31 | const [time, json] = cache.split(';') 32 | let cacheJson = null 33 | 34 | try { 35 | cacheJson = JSON.parse(json) 36 | } catch (e) { 37 | console.log('[Plugin Browser] Error parsing cache JSON: ', e) 38 | return null 39 | } 40 | 41 | // check and see if we can clear cache 42 | maybeClearCache(time) 43 | 44 | return cacheJson 45 | } 46 | 47 | export function maybeClearCache(time: string) { 48 | // If its been more than an hour, clear it 49 | if (Date.now() - parseInt(time) > 1000 * 60 * 60) { 50 | localStorage.removeItem('plugins-browser-cache') 51 | } 52 | } 53 | 54 | export function getPluginJsonCache() { 55 | const cache = localStorage.getItem('plugins-browser-plugin-json') 56 | if (!cache) { 57 | return {} 58 | } 59 | 60 | const [time, json] = cache.split(';') 61 | let cacheJson = null 62 | 63 | try { 64 | cacheJson = JSON.parse(json) 65 | } catch (e) { 66 | console.log('[Plugin Browser] Error parsing cache JSON: ', e) 67 | return {} 68 | } 69 | 70 | // check and see if we can clear cache 71 | maybeClearPluginJsonCache(time) 72 | 73 | return cacheJson 74 | } 75 | 76 | export function savePluginJsonCache(url: string, json: object) { 77 | localStorage.setItem('plugins-browser-plugin-json', `${Date.now()};${JSON.stringify({ ...getPluginJsonCache(), [url]: json })}`) 78 | } 79 | 80 | export function maybeClearPluginJsonCache(time: string) { 81 | // If its been more than an hour, clear it 82 | if (Date.now() - parseInt(time) > 1000 * 60 * 60) { 83 | localStorage.removeItem('plugins-browser-plugin-json') 84 | } 85 | } -------------------------------------------------------------------------------- /util/keyUtil.ts: -------------------------------------------------------------------------------- 1 | const Keycode = { 2 | 112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6', 118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12', 3 | 27: 'Escape', 32: 'Space', 4 | 17: 'Control', 16: 'Shift', 18: 'Alt', 5 | 91: 'Meta', 6 | 13: 'Enter', 38: 'Up', 40: 'Down', 37: 'Left', 39: 'Right', 8: 'Backspace', 7 | 20: 'CapsLock', 9: 'Tab', 36: 'Home', 35: 'End', 33: 'PageUp', 34: 'PageDown', 8 | 45: 'Insert', 46: 'Delete', 9 | 109: 'NumpadSubtract', 107: 'NumpadAdd', 111: 'NumpadDivide', 106: 'NumpadMultiply', 10 | 192: 'Grave', 189: 'Minus', 187: 'Equal', 219: 'LeftBracket', 221: 'RightBracket', 11 | 220: 'BackSlash', 186: 'Semicolon', 222: 'Apostrophe', 188: 'Comma', 190: 'Dot', 191: 'Slash' 12 | } 13 | 14 | // Convert a key code to a string 15 | export const keyToStr = (key: number) => { 16 | let keyStr = '' 17 | 18 | // get char code of uppercase letter 19 | if (key >= 65 && key <= 90) { 20 | keyStr = String.fromCharCode(key) 21 | } 22 | 23 | // get char code of lowercase letter 24 | if (key >= 97 && key <= 122) { 25 | keyStr = String.fromCharCode(key - 32) 26 | } 27 | 28 | // get char code of number 29 | if (key >= 48 && key <= 57) { 30 | keyStr = String.fromCharCode(key) 31 | } 32 | 33 | // Get everything else 34 | if (Keycode[key]) { 35 | keyStr = Keycode[key] 36 | } 37 | 38 | return keyStr 39 | } 40 | 41 | // Convert a key string to a key code 42 | export const strToKey = (str: string) => { 43 | let key = 0 44 | 45 | if (str.length === 1) { 46 | // get char code of lowercase letter 47 | if (str >= 'a' && str <= 'z') { 48 | return str.charCodeAt(0) - 32 49 | } 50 | 51 | return str.charCodeAt(0) 52 | } 53 | 54 | // Get everything else 55 | for (const [k, v] of Object.entries(Keycode)) { 56 | if (v === str) { 57 | key = parseInt(k) 58 | } 59 | } 60 | 61 | return key 62 | } 63 | 64 | // converts things like A to KeyA and 0 to Digit0 65 | export function strToCode(str: string) { 66 | if (str.length === 1) { 67 | // get char code of lowercase letter 68 | if (str.toLowerCase() >= 'a' && str.toLowerCase() <= 'z') { 69 | return 'Key' + str.toUpperCase() 70 | } 71 | 72 | return 'Digit' + str 73 | } 74 | 75 | let maybeKeycode = '' 76 | 77 | Object.values(Keycode).forEach((v) => { 78 | console.log('comparing', str, v) 79 | if (str.includes(v)) { 80 | console.log('found!') 81 | maybeKeycode = v 82 | } 83 | }) 84 | 85 | console.log(maybeKeycode) 86 | 87 | if (maybeKeycode) { 88 | return maybeKeycode 89 | } 90 | 91 | return 'Key' + str 92 | } -------------------------------------------------------------------------------- /plugins/web-keybinds/index.tsx: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | dispatcher, 4 | }, 5 | ui: { 6 | SwitchItem, 7 | }, 8 | plugin: { 9 | store, 10 | } 11 | } = shelter 12 | 13 | const isMac = navigator.userAgent.includes('Mac OS X') 14 | 15 | // @ts-expect-error this is defined (sometimes) 16 | if (window?.Vencord?.Plugins?.plugins?.WebKeybinds?.started) { 17 | throw new Error('Web Keybinds: plugin incompatibility (cannot run Vencord WebKeybinds alongside shelter Web Keybinds)') 18 | } 19 | 20 | // If this is the first time using this plugin, check if we are being used in a custom client and set desktopOnlyKeybinds accordingly 21 | if (store.desktopOnlyKeybinds === undefined) { 22 | const isCustomClient = [ 23 | window?.Dorion, 24 | // @ts-expect-error could exist 25 | window?.legcord, 26 | ].some(v => !!v) 27 | 28 | store.desktopOnlyKeybinds = isCustomClient 29 | } 30 | 31 | const handleKeyDown = (e: KeyboardEvent) => { 32 | const ctrl = e.ctrlKey || (isMac && e.metaKey) 33 | const key = e.key.toLowerCase() 34 | 35 | if (!ctrl) return 36 | 37 | switch(key) { 38 | case 't': 39 | if (!store.desktopOnlyKeybinds) return 40 | e.preventDefault() 41 | 42 | if (e.shiftKey) { 43 | // TODO open @me and the DM creation panel 44 | return 45 | } 46 | 47 | dispatcher.dispatch({ 48 | type: 'QUICKSWITCHER_SHOW', 49 | query: '', 50 | queryMode: null, 51 | }) 52 | 53 | break 54 | 55 | case 'tab': 56 | if (!store.desktopOnlyKeybinds) return 57 | e.preventDefault() 58 | 59 | // TODO handle guild selection prev/next 60 | break 61 | 62 | case ',': 63 | e.preventDefault() 64 | 65 | dispatcher.dispatch({ 66 | 'type': 'USER_SETTINGS_MODAL_OPEN', 67 | 'section': 'My Account', 68 | 'subsection': null, 69 | 'openWithoutBackstack': false 70 | }) 71 | 72 | dispatcher.dispatch({ 73 | type: 'LAYER_PUSH', 74 | component: 'USER_SETTINGS' 75 | }) 76 | break 77 | 78 | default: 79 | // TODO implement numbered keybinds 80 | break 81 | } 82 | } 83 | 84 | document.addEventListener('keydown', handleKeyDown) 85 | 86 | export const onUnload = () => { 87 | document.removeEventListener('keydown', handleKeyDown) 88 | } 89 | 90 | export const settings = () => ( 91 | <> 92 | { 95 | store.desktopOnlyKeybinds = v 96 | }} 97 | note="Enable keybinds that would otherwise interfere with browser keybinds. Intended for use in custom clients." 98 | > 99 | Desktop-only Keybinds 100 | 101 | > 102 | ) 103 | -------------------------------------------------------------------------------- /plugins/dorion-updater/index.tsx: -------------------------------------------------------------------------------- 1 | import { invoke, process, event, appName } from '../../api/api.js' 2 | 3 | const { 4 | ui: { 5 | openModal, 6 | ModalRoot, 7 | ModalHeader, 8 | ModalBody, 9 | ModalConfirmFooter 10 | }, 11 | } = shelter 12 | 13 | const confirmModal = (props: ConfirmationModalProps) => ( 14 | 15 | {props.header} 18 | {props.body} 19 | 26 | 27 | ) 28 | 29 | const load = async () => { 30 | console.log('[Updater] Checking for updates...') 31 | const config = JSON.parse(await invoke('read_config_file')) 32 | // This returns an array of what to update, if anything. 33 | const updateCheck = await invoke('update_check') 34 | 35 | const doUpdate = () => { 36 | invoke('do_update', { 37 | toUpdate: updateCheck, 38 | }) 39 | } 40 | 41 | console.log(`[Updater] ${appName} things to update: ${updateCheck}`) 42 | 43 | if (config.update_notify !== undefined && !config.update_notify) return 44 | 45 | if (updateCheck.includes('dorion')) { 46 | // If autoupdate is enabled, just do it, otherwise ask the user. 47 | if (config.autoupdate) { 48 | // We should still warn that Dorion is going to restart 49 | openModal((props) => confirmModal({ 50 | header: `${appName} Update`, 51 | body: `A ${appName} update has been fetched, and ${appName} will restart momentarily.`, 52 | confirmText: 'Got it!', 53 | type: 'neutral', 54 | onConfirm: () => doUpdate(), 55 | onCancel: props.close, 56 | })) 57 | 58 | doUpdate() 59 | return 60 | } 61 | 62 | openModal((props) => confirmModal({ 63 | header: 'Updates Available!', 64 | body: `There are ${appName} updates available. Would you like to apply them? This notification can be disabled in ${appName} Settings`, 65 | confirmText: 'Yes please!', 66 | cancelText: 'Nope!', 67 | type: 'neutral', 68 | onConfirm: () => doUpdate(), 69 | onCancel: props.close, 70 | })) 71 | } 72 | 73 | // Listen for update_complete event 74 | event.once('update_complete', () => { 75 | openModal((props) => confirmModal({ 76 | header: 'Update Complete!', 77 | body: 'The update has been applied! Please restart to apply the changes.', 78 | confirmText: 'Okay!', 79 | type: 'neutral', 80 | onConfirm: () => process.relaunch(), 81 | onCancel: props.close, 82 | })) 83 | }) 84 | } 85 | 86 | export const onUnload = () => {} 87 | 88 | load() -------------------------------------------------------------------------------- /plugins/dorion-ptt/index.ts: -------------------------------------------------------------------------------- 1 | import { keyToStr, strToCode } from '../../util/keyUtil.js' 2 | import { invoke, event } from '../../api/api.js' 3 | 4 | const { 5 | flux: { 6 | dispatcher: FluxDispatcher, 7 | stores: { 8 | MediaEngineStore 9 | } 10 | }, 11 | observeDom 12 | } = shelter 13 | 14 | const events = [] 15 | const subscriptions = [] 16 | const unobserves = [] 17 | const warningSelector = 'div[class*="warning__"]' 18 | const radiobarSelector = 'div[class*="radioBar_"]' 19 | const popupSelector = 'div[class*="layerContainer_"] div[class*="layer_"]' 20 | 21 | const unobserveAll = () => unobserves.forEach((unobserve) => unobserve()) 22 | 23 | const settingsHandler = async (payload) => { 24 | if (payload.section !== 'Voice & Video') { 25 | // Unobserve all 26 | unobserveAll() 27 | return 28 | } 29 | 30 | // This gets rid of the warning messages, as they dont apply anymore 31 | unobserves.push( 32 | observeDom(warningSelector, (node: HTMLDivElement) => { 33 | node.remove() 34 | }), 35 | observeDom(popupSelector, (node: HTMLDivElement) => { 36 | if (node.id) return 37 | 38 | // Remove the actual popout 39 | node.innerHTML = '' 40 | 41 | // Click the backdrop 42 | const unobserveBackdrop = observeDom('div[class*="scrim_"]', (backdrop: HTMLDivElement) => { 43 | backdrop.click() 44 | unobserveBackdrop() 45 | }) 46 | }), 47 | observeDom(radiobarSelector, (node: HTMLDivElement) => { 48 | const textSelector = 'div[class*="info_"] div[class*="text"]' 49 | const text = node.querySelector(textSelector) 50 | 51 | if (text.textContent.includes('(')) { 52 | text.textContent = text.textContent.replace(/\(.+?\)/g, '') 53 | } 54 | }) 55 | ) 56 | } 57 | 58 | // Handles the keybind thing 59 | const keybindCreationHandler = async (payload) => { 60 | const { 61 | mode, 62 | options: { 63 | shortcut 64 | } 65 | } = payload 66 | 67 | const keys = shortcut.map(k => k[1]) 68 | const toKeys = keys.map((k) => ({ 69 | code: strToCode(keyToStr(k)), 70 | name: keyToStr(k) 71 | })) 72 | 73 | invoke('set_keybind', { action: 'PUSH_TO_TALK', keys: toKeys }) 74 | 75 | event.emit('ptt_toggled', { 76 | state: mode === 'PUSH_TO_TALK' 77 | }) 78 | } 79 | 80 | // Initial set internally 81 | event.emit('ptt_toggled', { 82 | // @ts-expect-error shut up 83 | state: MediaEngineStore?.getMode?.() === 'PUSH_TO_TALK' 84 | }) 85 | 86 | subscriptions.push( 87 | FluxDispatcher.subscribe('USER_SETTINGS_MODAL_SET_SECTION', settingsHandler), 88 | FluxDispatcher.subscribe('LAYER_POP', unobserveAll), 89 | FluxDispatcher.subscribe('AUDIO_SET_MODE', keybindCreationHandler) 90 | ) 91 | 92 | export const onUnload = () => { 93 | unobserveAll() 94 | events.forEach((e) => e()) 95 | subscriptions.forEach((s) => s()) 96 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/components/ClientModList.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from '../../../api/api.js' 2 | import { css, classes } from './ClientModList.tsx.scss' 3 | 4 | const { 5 | ui: { SwitchItem, Text, injectCss }, 6 | solid: { createSignal }, 7 | } = shelter 8 | 9 | let injectedCss = false 10 | 11 | const getClientMods = async (): Promise => { 12 | try { 13 | return await invoke('available_mods') 14 | // eslint-disable-next-line 15 | } catch (e) { 16 | // function doesn't exist, version is too old 17 | } 18 | } 19 | 20 | interface Props { 21 | onChange: () => void 22 | } 23 | 24 | export function ClientModList(props: Props) { 25 | if (!injectedCss) { 26 | injectedCss = true 27 | injectCss(css) 28 | } 29 | 30 | const [settings, setSettingsState] = createSignal({ 31 | zoom: '1.0', 32 | client_type: 'default', 33 | sys_tray: false, 34 | push_to_talk: false, 35 | push_to_talk_keys: [], 36 | theme: 'none', 37 | use_native_titlebar: false, 38 | start_maximized: false, 39 | open_on_startup: false, 40 | startup_minimized: false, 41 | autoupdate: false, 42 | update_notify: true, 43 | multi_instance: false, 44 | client_mods: [], 45 | }) 46 | 47 | const [clientMods, setClientMods] = createSignal([]) 48 | 49 | ;(async () => { 50 | setSettingsState(JSON.parse(await invoke('read_config_file'))) 51 | setClientMods(await getClientMods()) 52 | 53 | console.log(settings()) 54 | })() 55 | 56 | function onClientModToggle(modName: string) { 57 | const newClientMods = [...settings().client_mods] 58 | 59 | if (newClientMods.includes(modName)) { 60 | newClientMods.splice(newClientMods.indexOf(modName), 1) 61 | } else { 62 | newClientMods.push(modName) 63 | } 64 | 65 | setSettings((s) => ({ ...s, client_mods: newClientMods })) 66 | 67 | props.onChange() 68 | } 69 | 70 | const setSettings = (fn: (DorionSettings) => DorionSettings) => { 71 | setSettingsState(fn(settings())) 72 | 73 | // Save the settings 74 | invoke('write_config_file', { 75 | contents: JSON.stringify(fn(settings())), 76 | }) 77 | } 78 | 79 | return <> 80 | {clientMods().length === 0 && ( 81 | 82 | Client mods not available. Please update 83 | 84 | )} 85 | 86 | {clientMods().map((modName: string) => ( 87 | 91 | onClientModToggle(modName) 92 | } 93 | note={modName === 'Shelter' ? 'Shelter is required for Dorion to function properly.' : ''} 94 | > 95 | Enable {modName} 96 | 97 | ))} 98 | > 99 | } 100 | -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/ChangelogPage.tsx.scss: -------------------------------------------------------------------------------- 1 | .tophead { 2 | margin-bottom: 16px; 3 | } 4 | 5 | .refresh { 6 | position: absolute; 7 | top: 52px; 8 | right: 40px; 9 | } 10 | 11 | .card { 12 | padding: 16px; 13 | border-radius: 8px; 14 | color: var(--text-default); 15 | background: var(--background-surface-highest); 16 | position: relative; 17 | min-height: 82px; 18 | 19 | &:not(:last-child) { 20 | margin-bottom: 16px; 21 | } 22 | 23 | button { 24 | margin-top: 16px; 25 | width: 100%; 26 | } 27 | 28 | .title { 29 | margin-bottom: 8px; 30 | font-size: 1.5rem; 31 | font-weight: 500; 32 | } 33 | 34 | .badges { 35 | position: absolute; 36 | top: 16px; 37 | right: 16px; 38 | display: flex; 39 | flex-direction: row; 40 | gap: 8px; 41 | 42 | .badge { 43 | padding: 0 8px; 44 | font-size: 0.75rem; 45 | border-radius: 3px; 46 | background-color: var(--status-positive-background); 47 | } 48 | } 49 | 50 | .spinner { 51 | $size: 50px; 52 | box-sizing: border-box; 53 | content: ""; 54 | position: absolute; 55 | top: calc(50% - $size / 2); 56 | left: calc(50% - $size / 2); 57 | width: $size; 58 | height: $size; 59 | border: 10px solid transparent; 60 | border-top-color: var(--text-secondary); 61 | border-radius: 50%; 62 | animation: spin 1s linear infinite; 63 | } 64 | 65 | .contents { 66 | margin-top: 16px; 67 | 68 | h1, 69 | h2 { 70 | color: var(--header-primary); 71 | font-weight: 500; 72 | margin-bottom: 8px; 73 | 74 | &:not(:first-child) { 75 | margin-top: 24px; 76 | } 77 | } 78 | 79 | h1 { 80 | font-size: 1.25rem; 81 | } 82 | 83 | h2 { 84 | font-size: 1rem; 85 | } 86 | 87 | img { 88 | display: block; 89 | max-width: 100%; 90 | margin: 0 auto; 91 | 92 | &[src^="http"], 93 | &[src^="https"] { 94 | display: block; 95 | font-size: 0.75rem; 96 | margin-top: 8px; 97 | color: var(--text-secondary); 98 | background: var(--background-base-lowest); 99 | position: relative; 100 | border-radius: 8px; 101 | padding: 16px; 102 | height: 82px; 103 | 104 | &::before { 105 | @extend .spinner; 106 | } 107 | } 108 | } 109 | 110 | p, 111 | ul, 112 | ol, 113 | summary, 114 | img { 115 | font-size: 0.875rem; 116 | line-height: 1.25rem; 117 | 118 | &:not(:last-child) { 119 | margin-bottom: 6px; 120 | } 121 | } 122 | 123 | ul, 124 | ol { 125 | padding-left: 32px; 126 | 127 | li { 128 | list-style-type: initial; 129 | } 130 | } 131 | } 132 | 133 | @keyframes spin { 134 | 0% { 135 | transform: rotate(0deg); 136 | } 137 | 138 | 100% { 139 | transform: rotate(360deg); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /plugins/orbolay/settings.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from '../../components/Dropdown.jsx' 2 | import { Config, defaultConfig } from './index.js' 3 | import { classes, css } from './settings.scss' 4 | 5 | const { 6 | ui: { 7 | injectCss, 8 | SwitchItem, 9 | Text, 10 | TextBox, 11 | Divider 12 | }, 13 | plugin: { 14 | store 15 | } 16 | } = shelter 17 | 18 | interface Props { 19 | ws?: WebSocket 20 | } 21 | 22 | let injectedCss = false 23 | 24 | export const Settings = (props: Props) => { 25 | if (!injectedCss) { 26 | injectedCss = true 27 | injectCss(css) 28 | } 29 | 30 | const submitSettings = () => { 31 | props?.ws?.send?.(JSON.stringify({ 32 | cmd: 'REGISTER_CONFIG', 33 | ...store 34 | })) 35 | } 36 | 37 | const set = (key: keyof Config, value: unknown) => { 38 | store[key] = value 39 | submitSettings() 40 | } 41 | 42 | return ( 43 | <> 44 | 45 | Orbolay Port 46 | set('port', parseInt(v) || defaultConfig.port)} 49 | type="number" 50 | /> 51 | 52 | 53 | 54 | set('keybindIsEnabled', v)} 57 | > 58 | Enable Global Keybind 59 | 60 | 61 | 62 | Messages Alignment 63 | set('messageAlignment', e.target.value)} 67 | options={[ 68 | { label: 'Top Left', value: 'topleft' }, 69 | { label: 'Top Right', value: 'topright' }, 70 | { label: 'Bottom Left', value: 'bottomleft' }, 71 | { label: 'Bottom Right', value: 'bottomright' }, 72 | ]} 73 | /> 74 | 75 | 76 | 77 | 78 | User Alignment 79 | set('userAlignment', e.target.value)} 83 | options={[ 84 | { label: 'Top Left', value: 'topleft' }, 85 | { label: 'Top Right', value: 'topright' }, 86 | { label: 'Bottom Left', value: 'bottomleft' }, 87 | { label: 'Bottom Right', value: 'bottomright' }, 88 | ]} 89 | /> 90 | 91 | 92 | 93 | set('voiceSemitransparent', v)} 96 | > 97 | VC Members Semi-Transparent 98 | 99 | 100 | set('messagesSemitransparent', v)} 103 | > 104 | Message Notifications Semi-Transparent 105 | 106 | > 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /plugins/dorion-settings/util/changelog.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked' 2 | import { TReleases } from '../types/release.js' 3 | import { api, appName } from '../../../api/api.js' 4 | 5 | const { 6 | ui: { showToast }, 7 | plugins: { installedPlugins } 8 | } = shelter 9 | 10 | const devModeReservedId = '__DEVMODE_PLUGIN_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' 11 | 12 | function isDevMode(): boolean { 13 | return installedPlugins() && devModeReservedId in installedPlugins() 14 | } 15 | 16 | export async function loadChangelog(): Promise { 17 | ///NOTE - This is a thing for development. Otherwise GitHub will rate limit us :) 18 | if (isDevMode()) { 19 | console.warn(`[${appName} Changelog] Dev mode is on. Loading changelog from local storage.`) 20 | return loadChangelogFromLocalStorage() 21 | } 22 | 23 | try { 24 | const changelog = await fetchChangelogFromGitHub() 25 | saveChangelogToLocalStorage(changelog) 26 | return changelog 27 | } 28 | catch (e) { 29 | console.error(e) 30 | showToast({ 31 | title: 'Failed to load changelog', 32 | content: e.message, 33 | duration: 3000, 34 | }) 35 | 36 | return loadChangelogFromLocalStorage() 37 | } 38 | } 39 | 40 | async function fetchChangelogFromGitHub(): Promise { 41 | const response = await fetch(`https://api.github.com/repos/SpikeHD/${appName}/releases`, { 42 | headers: { 43 | 'User-Agent': appName 44 | } 45 | }) 46 | 47 | if (!response.ok) { 48 | throw new Error(`Failed to fetch changelog. ${response.status} ${response.statusText}`) 49 | } 50 | 51 | return await response.json() as TReleases 52 | } 53 | 54 | function loadChangelogFromLocalStorage(): TReleases { 55 | const changelog = localStorage.getItem('changelog') 56 | if (!changelog) return [] 57 | return JSON.parse(changelog) 58 | } 59 | 60 | function saveChangelogToLocalStorage(changelog: TReleases): void { 61 | localStorage.setItem('changelog', JSON.stringify(changelog)) 62 | } 63 | 64 | export async function processReleaseBodies(releases: TReleases): Promise { 65 | const processedReleases = await Promise.all(releases.map(async (release) => { 66 | release.body = await processReleaseBody(release.body) 67 | return release 68 | })) 69 | 70 | return processedReleases 71 | } 72 | 73 | export async function processReleaseBody(body: string): Promise { 74 | const parsedBody = await marked.parse(body) 75 | 76 | return parsedBody 77 | .replace('\n', '') // remove newlines. It's converted to html, so it's not needed 78 | .replace(/(\d+);/g, (_, code) => String.fromCharCode(code)) // Fix ascii references (such as ') 79 | .replace(/@([\w-]+)/g, '@$1') // GitHub user 80 | .replace(/#(\d+)/g, `#$1`) // GitHub issue or PR 81 | .replace(/([^<]+)<\/a>/g, '$2') // External link 82 | } 83 | 84 | export async function fixImageLinks(scope: HTMLElement): Promise { 85 | if (!scope) return 86 | 87 | const images = scope.getElementsByTagName('img') 88 | 89 | await Promise.all(Array.from(images).map(async (image: HTMLImageElement) => { 90 | const url = image.src 91 | image.src = await api.util.fetchImage(url) 92 | })) 93 | } 94 | -------------------------------------------------------------------------------- /plugins/dorion-settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { app, appName, invoke } from '../../api/api.js' 2 | 3 | import { PerformancePage } from './pages/PerformancePage.jsx' 4 | import { ProfilesPage } from './pages/ProfilesPage.jsx' 5 | import { SettingsPage } from './pages/SettingsPage.jsx' 6 | import { ChangelogPage } from './pages/ChangelogPage.jsx' 7 | import { PluginsPage } from './pages/PluginsPage.jsx' 8 | import { ThemesPage } from './pages/ThemesPage.jsx' 9 | import { RPCPage } from './pages/RPC.jsx' 10 | 11 | const { 12 | settings: { 13 | registerSection, 14 | }, 15 | flux: { 16 | dispatcher 17 | }, 18 | util: { 19 | sleep 20 | }, 21 | } = shelter 22 | 23 | let settingsUninjects = [] 24 | 25 | ;(async () => { 26 | // @ts-expect-error womp womp 27 | const platform = await window.__TAURI__.core.invoke('get_platform') 28 | 29 | settingsUninjects = [ 30 | registerSection('divider'), 31 | registerSection('header', appName), 32 | registerSection('section', `${appName}-settings`, `${appName} Settings`, SettingsPage), 33 | registerSection('section', `${appName}-plugins`, 'Plugins', PluginsPage), 34 | registerSection('section', `${appName}-themes`, 'Themes', ThemesPage), 35 | registerSection('section', `${appName}-performance`, 'Performance & Extras', PerformancePage), 36 | platform !== 'macos' && registerSection('section', `${appName}-rpc`, 'Rich Presence', RPCPage), 37 | registerSection('section', `${appName}-profiles`, 'Profiles', ProfilesPage), 38 | ] 39 | })() 40 | 41 | const appendAppVersion = async () => { 42 | let tries = 0 43 | const infoBoxSelector = 'div[class*="side_"] div[class*="info_"]' 44 | const hash = await invoke('git_hash').catch((e) => console.error(e)) || '' 45 | 46 | // Wait for infoBox to exist 47 | while (!document.querySelector(infoBoxSelector)) { 48 | await sleep(500) 49 | tries++ 50 | 51 | if (tries > 5) { 52 | console.error('Failed to find infoBox') 53 | return 54 | } 55 | } 56 | 57 | const versionThings = document.querySelector(infoBoxSelector) 58 | const firstChild = versionThings?.firstElementChild as HTMLSpanElement 59 | const newVersionThing = document.createElement('span') as HTMLSpanElement 60 | 61 | if (!firstChild) return 62 | 63 | newVersionThing.innerHTML = `${appName} v${await app.getVersion()}` 64 | 65 | if (hash) { 66 | newVersionThing.innerHTML += ` (${hash.slice(0, 7)})` 67 | } 68 | 69 | // @ts-expect-error This works 70 | newVersionThing.classList.add(...firstChild.classList) 71 | newVersionThing.style.color = firstChild.style.color 72 | newVersionThing.style.textTransform = 'none' 73 | 74 | versionThings.appendChild(newVersionThing) 75 | } 76 | 77 | const checkForUpdates = async () => { 78 | const updateCheck = await invoke('update_check') 79 | let needsUpdate = false 80 | 81 | if (updateCheck.includes('dorion')) needsUpdate = true 82 | 83 | settingsUninjects.push( 84 | registerSection('section', `${appName}-changelog`, 'Changelog', ChangelogPage, { 85 | badgeCount: needsUpdate ? 1 : 0 86 | }) 87 | ) 88 | } 89 | 90 | dispatcher.subscribe('USER_SETTINGS_MODAL_OPEN', appendAppVersion) 91 | 92 | checkForUpdates() 93 | 94 | export const onUnload = () => { 95 | settingsUninjects.forEach((u) => u && u()) 96 | dispatcher.unsubscribe('USER_SETTINGS_MODAL_OPEN', appendAppVersion) 97 | } -------------------------------------------------------------------------------- /plugins/invisible-typing/index.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './index.scss' 2 | 3 | const { 4 | flux: { 5 | intercept 6 | }, 7 | plugin: { 8 | store 9 | }, 10 | solid: { 11 | createSignal 12 | }, 13 | ui: { 14 | injectCss, 15 | tooltip 16 | }, 17 | observeDom 18 | } = shelter 19 | 20 | // eslint-disable-next-line 21 | false && tooltip 22 | 23 | let injectedCss = false 24 | 25 | if (!injectedCss) { 26 | injectedCss = true 27 | injectCss(css) 28 | } 29 | 30 | const keyboardSvg = ( 31 | 32 | { /* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. */ } 33 | 34 | 35 | ) 36 | 37 | const unintercept = intercept(dispatch => { 38 | if (dispatch.type === 'TYPING_START_LOCAL') return store.enabled ? false : null 39 | }) 40 | 41 | const unobserve = observeDom('[class^="channelTextArea"] [class^="buttons"]', (node) => { 42 | if (document.querySelector('#invis-icon')) return 43 | 44 | const [enabled, setEnabled] = createSignal(!!store.enabled) 45 | 46 | const toggleEnabled = () => { 47 | store.enabled = !enabled() 48 | setEnabled(!enabled()) 49 | } 50 | 51 | const invisIcon = ( 52 | 59 | {keyboardSvg} 60 | 61 | ) 62 | 63 | node.prepend(invisIcon) 64 | }) 65 | 66 | export const onUnload = () => { 67 | unintercept() 68 | unobserve() 69 | 70 | // Get rid of invis icon if it's still around 71 | const invisIcon = document.querySelector('#invis-icon') 72 | if (invisIcon) invisIcon.remove() 73 | } 74 | -------------------------------------------------------------------------------- /plugins/inline-css/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { CodeInput } from '@srsholmes/solid-code-input' 2 | import hljs from 'highlight.js/lib/core' 3 | import cssModule from 'highlight.js/lib/languages/css' 4 | 5 | import {css, classes} from './Editor.scss' 6 | import { debounce } from '../../../util/debounce.js' 7 | import { Popout } from './Popout.jsx' 8 | import { Window } from './Window.jsx' 9 | 10 | interface Props { 11 | styleElm?: HTMLStyleElement 12 | popout?: boolean 13 | } 14 | 15 | hljs.registerLanguage('css', cssModule) 16 | 17 | const { 18 | ui: { 19 | injectCss, 20 | Header, 21 | HeaderTags, 22 | Button, 23 | CheckboxItem 24 | }, 25 | plugin: { store }, 26 | solid: { createSignal, createEffect }, 27 | flux: { 28 | dispatcher 29 | } 30 | } = shelter 31 | 32 | const saveCss = debounce((css: string, styleElm: HTMLStyleElement) => { 33 | store.inlineCss = css 34 | 35 | if (styleElm) { 36 | styleElm.textContent = css 37 | } 38 | }, 500) 39 | 40 | let injectedCss = false 41 | 42 | export default function (props: Props) { 43 | // eslint-disable-next-line prefer-const 44 | let ref = null 45 | 46 | if (!injectedCss) { 47 | injectCss(css) 48 | injectedCss = true 49 | } 50 | 51 | const [inlineCss, setInlineCss] = createSignal('') 52 | const [hotReload, setHotReload] = createSignal(true) 53 | 54 | createEffect(() => { 55 | setInlineCss(store.inlineCss) 56 | }) 57 | 58 | const setCss = (css: string) => { 59 | if (ref) { 60 | // Find the textarea in the ref, and autoscroll down 61 | const textarea = ref.querySelector('textarea') 62 | if (textarea && textarea.scrollTop !== textarea.scrollHeight) { 63 | textarea.scrollTop = textarea.scrollHeight 64 | } 65 | } 66 | 67 | setInlineCss(css) 68 | saveCss(css, props.styleElm) 69 | } 70 | 71 | return ( 72 | <> 73 | CSS Editor 74 | 75 | { 76 | !props.popout && ( 77 | { 80 | document.body.appendChild( 81 | Window() 82 | ) 83 | 84 | // This closes settings automagically 85 | dispatcher.dispatch({ 86 | type: 'LAYER_POP' 87 | }) 88 | }} 89 | > 90 | Pop Out 91 | 92 | 93 | ) 94 | } 95 | 96 | 97 | 101 | Hot Reload 102 | 103 | 104 | { 106 | // Save inline CSS 107 | setCss(inlineCss()) 108 | }} 109 | disabled={hotReload()} 110 | > 111 | Save & Apply 112 | 113 | 114 | 115 | 116 | 125 | 126 | > 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | interface DorionSettings { 2 | // Deprecated 3 | theme: string; 4 | 5 | themes: string[]; 6 | zoom: string; 7 | client_type: string; 8 | sys_tray: boolean; 9 | push_to_talk: boolean; 10 | push_to_talk_keys: string[]; 11 | cache_css: boolean; 12 | use_native_titlebar: boolean; 13 | start_maximized: boolean; 14 | profile: string; 15 | streamer_mode_detection: boolean; 16 | rpc_server: boolean; 17 | open_on_startup: boolean; 18 | startup_minimized: boolean; 19 | autoupdate: boolean; 20 | update_notify: boolean; 21 | desktop_notifications: boolean; 22 | auto_clear_cache: boolean; 23 | multi_instance: boolean; 24 | disable_hardware_accel: boolean; 25 | blur: string; 26 | blur_css: boolean; 27 | client_mods: string[]; 28 | unread_badge: boolean; 29 | client_plugins: boolean; 30 | 31 | tray_icon_enabled: boolean; 32 | proxy_uri: string; 33 | 34 | keybinds: Record; 35 | keybinds_enabled: boolean; 36 | 37 | win7_style_notifications: boolean; 38 | 39 | // RPC-specific options 40 | rpc_process_scanner: boolean; 41 | rpc_ipc_connector: boolean; 42 | rpc_websocket_connector: boolean; 43 | rpc_secondary_events: boolean; 44 | } 45 | 46 | interface DorionTheme { 47 | label: string 48 | value: string 49 | } 50 | 51 | interface DorionPluginList { 52 | // Key is the filename 53 | [key: string]: { 54 | name: string 55 | preload: boolean 56 | enabled: boolean 57 | } 58 | } 59 | 60 | interface ShelteRPCPreviouslyPlayed { 61 | name: string 62 | lastPlayed: number 63 | hide: boolean 64 | local?: boolean 65 | appid: string 66 | } 67 | 68 | interface ShelteRPCStore { 69 | currentlyPlaying: string 70 | previouslyPlayed: ShelteRPCPreviouslyPlayed[] 71 | } 72 | 73 | interface ProcessWindow { 74 | title: string 75 | pid: string 76 | process_name: string 77 | } 78 | 79 | interface BasicModalProps { 80 | header: string 81 | body: string 82 | } 83 | 84 | interface ConfirmationModalProps { 85 | header: string 86 | body: string 87 | confirmText: string 88 | cancelText?: string 89 | onConfirm?: () => void 90 | onCancel?: () => void 91 | type?: 'neutral' | 'danger' | 'warning' 92 | } 93 | 94 | interface Backend { 95 | name: string 96 | invoke: (name: string, args?: any) => Promise 97 | event: { 98 | emit: (name: string, args?: any) => void 99 | listen: (name: string, cb: (args: any) => void) => Promise 100 | } 101 | app: { 102 | getVersion: () => string 103 | } 104 | process: { 105 | relaunch: () => void 106 | } 107 | apiWindow: { 108 | appWindow: { 109 | setFullscreen: (isFullscreen: boolean) => void 110 | } 111 | } 112 | } 113 | 114 | interface KeyStruct { 115 | name: string 116 | code: string 117 | } 118 | 119 | interface Keybind { 120 | key: string 121 | keys: KeyStruct[] 122 | } 123 | 124 | interface KeybindActionType { 125 | label: string 126 | value: string 127 | } 128 | 129 | interface KeybindDescription { 130 | [key: string]: string 131 | } 132 | 133 | interface KeybindActionsInternal { 134 | [key: string]: { 135 | storeValue?: { 136 | store: string 137 | key: string 138 | eventKey: string 139 | modify: (event: any, store: any) => any 140 | } 141 | press?: { 142 | [key: string]: any 143 | }[] 144 | release?: { 145 | [key: string]: any 146 | }[] 147 | } 148 | } -------------------------------------------------------------------------------- /plugins/dorion-theme-browser/components/ThemePage.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from '../../../components/Dropdown.jsx' 2 | import { debounce } from '../../../util/debounce.js' 3 | import { themeListEndpoint } from '../api.js' 4 | import { ThemeCard } from './ThemeCard.jsx' 5 | import { css, classes } from './ThemePage.tsx.scss' 6 | 7 | const { 8 | ui: { 9 | injectCss, 10 | Divider, 11 | Header, 12 | HeaderTags, 13 | TextBox 14 | }, 15 | solid: { 16 | createSignal, 17 | createEffect, 18 | }, 19 | } = shelter 20 | 21 | let injectedCss = false 22 | 23 | export function ThemePage() { 24 | if (!injectedCss) { 25 | injectCss(css) 26 | injectedCss = true 27 | } 28 | 29 | const [themeData, setThemeData] = createSignal([]) 30 | const [page, setPage] = createSignal(1) 31 | const [sort, setSort] = createSignal('popular') 32 | const [search, setSearch] = createSignal('') 33 | 34 | createEffect(async () => { 35 | await loadThemes() 36 | }) 37 | 38 | const loadThemes = async () => { 39 | setThemeData(await themeListEndpoint({ page: page().toString(), sort: sort(), filter: search() })) 40 | } 41 | 42 | const doSearch = debounce((v: string) => setSearch(v), 500) 43 | 44 | return ( 45 | <> 46 | Theme Browser 47 | 48 | 49 | { 52 | setSort(e.target.value) 53 | loadThemes() 54 | }} 55 | style='width: 30%;' 56 | options={[ 57 | { label: 'Popular', value: 'popular' }, 58 | { label: 'Creation Date', value: 'creationdate' }, 59 | { label: 'Name', value: 'name' }, 60 | { label: 'Likes', value: 'likes' }, 61 | { label: 'Downloads', value: 'downloads' }, 62 | { label: 'Recently Updated', value: 'recentlyupdated' }, 63 | ]} 64 | placeholder={'Sort by...'} 65 | /> 66 | 67 | 68 | doSearch(v)} 71 | placeholder={'Search...'} 72 | /> 73 | 74 | 75 | 76 | 77 | 78 | 79 | { 80 | themeData().map((t) => ( 81 | 91 | )) 92 | } 93 | 94 | 95 | 96 | 97 | { 100 | setPage(page() - 1) 101 | loadThemes() 102 | }} 103 | >< Previous 104 | 105 | setPage(parseInt(e.target.value))} /> 106 | 107 | { 110 | setPage(page() + 1) 111 | loadThemes() 112 | }} 113 | >Next > 114 | 115 | 116 | > 117 | ) 118 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/components/PluginList.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from '../../../api/api.js' 2 | import { css, classes } from './PluginList.tsx.scss' 3 | 4 | const { 5 | ui: { Switch, Text, injectCss }, 6 | solid: { createSignal }, 7 | } = shelter 8 | 9 | let injectedCss = false 10 | 11 | const getPlugins = async () => { 12 | const plugins: DorionPluginList = await invoke('get_plugin_list') 13 | return plugins 14 | } 15 | 16 | interface Props { 17 | onChange: () => void 18 | } 19 | 20 | export function PluginList(props: Props) { 21 | if (!injectedCss) { 22 | injectedCss = true 23 | injectCss(css) 24 | } 25 | 26 | const [plugins, setPlugins] = createSignal({}) 27 | 28 | ;(async () => { 29 | setPlugins(await getPlugins()) 30 | })() 31 | 32 | return ( 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Enabled 46 | 47 | 48 | 49 | 50 | 51 | Preload 52 | 53 | 54 | 55 | 56 | {Object.entries(plugins() as DorionPluginList).length === 0 && ( 57 | 58 | 59 | No plugins found 60 | 61 | 62 | )} 63 | 64 | {Object.entries(plugins() as DorionPluginList).map(([filename, plugin]) => ( 65 | 66 | 67 | {plugin.name} 68 | 69 | 70 | 71 | { 74 | props.onChange() 75 | 76 | invoke('toggle_plugin', { 77 | name: filename, 78 | }) 79 | 80 | setPlugins( 81 | { 82 | ...plugins(), 83 | [filename]: { 84 | ...plugin, 85 | enabled: !plugin.enabled, 86 | }, 87 | } 88 | ) 89 | }} 90 | style={{ 91 | flexDirection: 'column-reverse', 92 | marginRight: '4px' 93 | }} 94 | /> 95 | 96 | 97 | 98 | { 102 | props.onChange() 103 | 104 | invoke('toggle_preload', { 105 | name: filename, 106 | }) 107 | 108 | setPlugins( 109 | { 110 | ...plugins(), 111 | [filename]: { 112 | ...plugin, 113 | preload: !plugin.preload, 114 | }, 115 | } 116 | ) 117 | }} 118 | style={{ 119 | marginRight: '4px' 120 | }} 121 | /> 122 | 123 | 124 | ))} 125 | 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /plugins/dorion-titlebar/waitElm.ts: -------------------------------------------------------------------------------- 1 | const { 2 | util: { log } 3 | } = shelter 4 | 5 | let observer: MutationObserver | null = null // keep only one observer working 6 | 7 | export function disobserve() { 8 | observer.disconnect() 9 | observer = null 10 | } 11 | 12 | // Observes the DOM for newly added nodes and executes a callback for each. 13 | function observeDom(rootElm: Node, callbackFn: (node: Node, resolve: (value: T) => void) => boolean, subtree: boolean): Promise { 14 | return new Promise(resolve => { 15 | if (observer) disobserve() // disconnnect old one 16 | 17 | observer = new MutationObserver(mutations => { 18 | for (const mutation of mutations) { 19 | if (mutation.type === 'childList') { 20 | const addedNodes = Array.from(mutation.addedNodes) 21 | for (const node of addedNodes) { 22 | if (!callbackFn(node, resolve)) { 23 | return disobserve() 24 | } 25 | } 26 | } 27 | } 28 | }) 29 | 30 | observer.observe(rootElm, { 31 | childList: true, 32 | subtree // reduce callback count for perf 33 | }) 34 | }) 35 | } 36 | 37 | type Query = Array | string 38 | type WaitCfg = { callbackFn: null | ((elm: Element) => void), root: Element } 39 | const subtreeFind = (p: Element, q: Array) => Array.from(p.children).find(c => q.some(q => c.matches(q))) 40 | const queryFind = (p: Element, query: Array) => { 41 | for (let q of query) { 42 | const subtree = q[0] === '>' 43 | if (subtree) q = q.slice(1) 44 | const elm = subtree ? subtreeFind(p, [q]) : p.querySelector(q) 45 | if (elm) return elm 46 | } 47 | } 48 | 49 | export const waitForElm = async (queries: Array | Query, cfg: Partial): Promise => { 50 | let root = cfg.root || document.body 51 | const callbackFn = cfg.callbackFn 52 | 53 | let query: string[] 54 | let timeout = true 55 | const startTimeout = () => setTimeout(() => { 56 | if (timeout) { 57 | log(['The observer seems stuck at', root, 'looking for', query, 'with remaining queries:', queries], 'warn') 58 | startTimeout() 59 | } 60 | }, 10000) 61 | 62 | startTimeout() 63 | 64 | if (!Array.isArray(queries)) queries = [queries] 65 | 66 | while (queries.length) { 67 | // prepare query 68 | const q: Query = queries.shift() 69 | query = typeof q === 'string' ? [q] : q 70 | const directChild = query.every(q => q[0] === '>') 71 | 72 | if (directChild) query = query.map(q => q.slice(1)) 73 | 74 | // no observe if this elm already exist 75 | const elm = directChild ? subtreeFind(root, query) : queryFind(root, query) 76 | 77 | if (elm) { 78 | root = elm 79 | if (callbackFn) callbackFn(root) 80 | continue 81 | } 82 | 83 | // start observer 84 | root = await observeDom(root, (node, res) => { 85 | if (node.nodeType !== Node.ELEMENT_NODE) return true 86 | 87 | const e = node as Element 88 | 89 | for (let q of query) { 90 | if (!directChild) { 91 | const s = q[0] === '>' 92 | if (s) q = q.slice(1) 93 | } 94 | 95 | let ret = e.matches(q) ? e : null 96 | 97 | if (!ret) { 98 | ret = e.querySelector(q) 99 | } 100 | 101 | if (ret) { 102 | res(e) 103 | return false 104 | } 105 | } 106 | 107 | return true 108 | }, !directChild) as Element 109 | 110 | // callback after found 111 | if (callbackFn) callbackFn(root) 112 | } 113 | 114 | timeout = false 115 | 116 | return root 117 | } -------------------------------------------------------------------------------- /components/KeybindInput.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './KeybindInput.tsx.scss' 2 | 3 | const { 4 | solid: { 5 | createSignal, 6 | onCleanup 7 | }, 8 | ui: { 9 | Text, 10 | injectCss 11 | } 12 | } = shelter 13 | 14 | interface Props { 15 | initialKeybind: KeyStruct[] 16 | onKeybindChange(keybind: KeyStruct[]): void 17 | 18 | // style overrides 19 | style?: string 20 | disabled?: boolean 21 | } 22 | 23 | let injectedCss = false 24 | 25 | export function KeybindInput(props: Props) { 26 | if (!injectedCss) { 27 | injectedCss = true 28 | injectCss(css) 29 | } 30 | 31 | const [recording, setRecording] = createSignal(false) 32 | const [keybind, setKeybind] = createSignal(props.initialKeybind || []) 33 | const [keysPressed, setKeysPressed] = createSignal([]) 34 | 35 | onCleanup(() => { 36 | window.removeEventListener('keydown', keyDown) 37 | window.removeEventListener('keyup', keyUp) 38 | }) 39 | 40 | const keyDown = (e) => { 41 | const keycode = { 42 | name: e.key, 43 | code: e.code, 44 | } 45 | 46 | setKeysPressed([...keysPressed(), keycode]) 47 | 48 | if (keycode.name.length === 1) { 49 | keycode.name = keycode.name.toUpperCase() 50 | } 51 | 52 | // clear keybind if its a new keybind 53 | if (keysPressed().length === 1) { 54 | setKeybind([keycode]) 55 | return 56 | } 57 | 58 | // If the key is already in the keybind, don't add it again 59 | if (keybind().find((k) => k.code === keycode.code)) { 60 | return 61 | } 62 | 63 | // if ctrl, alt, or shift, add it to the front of the keybind 64 | switch (e.key) { 65 | case 'Control': 66 | case 'Alt': 67 | case 'Shift': 68 | case 'Meta': 69 | setKeybind([keycode, ...keybind()]) 70 | break 71 | default: 72 | setKeybind([...keybind(), keycode]) 73 | } 74 | } 75 | 76 | const keyUp = (e) => { 77 | const keycode = { 78 | name: e.key, 79 | code: e.code, 80 | } 81 | 82 | setKeysPressed(keysPressed().filter((k) => k.code !== keycode.code)) 83 | } 84 | 85 | const setRecordingState = () => { 86 | if (recording()) { 87 | // Remove all event listeners 88 | window.removeEventListener('keydown', keyDown) 89 | window.removeEventListener('keyup', keyUp) 90 | 91 | // Set the keybind 92 | props.onKeybindChange(keybind()) 93 | 94 | setRecording(false) 95 | 96 | return 97 | } 98 | 99 | // Clear the keybind 100 | setKeybind([]) 101 | 102 | // Create event listeners to set the keybind based on what is being held down 103 | window.addEventListener('keydown', keyDown) 104 | window.addEventListener('keyup', keyUp) 105 | 106 | setRecording(true) 107 | } 108 | 109 | return ( 110 | 114 | 115 | 116 | { 117 | keybind().length ? keybind().map((k, i) => { 118 | return i === keybind().length - 1 ? k.name : k.name + ' + ' 119 | }) : 'No Keybind Set' 120 | } 121 | 122 | 123 | 124 | { 127 | if (props.disabled) return 128 | 129 | setRecordingState() 130 | }} 131 | > 132 | {recording() ? 'Stop Recording' : 'Edit Keybind'} 133 | 134 | 135 | ) 136 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/ThemesPage.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from '../../../api/api.js' 2 | import { reloadThemes } from '../../../util/theme.js' 3 | import { Dropdown } from '../../../components/Dropdown.jsx' 4 | import { css, classes } from './ThemesPage.tsx.scss' 5 | import { installThemeModal } from '../util/theme.jsx' 6 | import { defaultConfig } from '../util/settings.js' 7 | 8 | const { 9 | ui: { Header, Button, HeaderTags, injectCss, Divider, ButtonSizes }, 10 | solid: { createSignal, createEffect }, 11 | } = shelter 12 | 13 | let injectedCss = false 14 | 15 | export function ThemesPage() { 16 | if (!injectedCss) { 17 | injectedCss = true 18 | injectCss(css) 19 | } 20 | 21 | const [settings, setSettingsState] = createSignal(defaultConfig) 22 | const [themes, setThemes] = createSignal([]) 23 | 24 | const getThemes = async () => { 25 | const themes: string[] = await invoke('get_theme_names') 26 | return themes.map((t: string) => ({ 27 | label: t.replace(/"/g, '').replace('.css', '').replace('.theme', ''), 28 | value: t.replace(/"/g, ''), 29 | })) 30 | } 31 | 32 | createEffect(async () => { 33 | const settings = JSON.parse(await invoke('read_config_file')) 34 | 35 | if (!settings.themes) { 36 | settings.themes = [] 37 | } 38 | 39 | setSettingsState(settings) 40 | setThemes(await getThemes()) 41 | }) 42 | 43 | const setSettings = (fn: (DorionSettings) => DorionSettings) => { 44 | setSettingsState(fn(settings())) 45 | 46 | // Save the settings 47 | invoke('write_config_file', { 48 | contents: JSON.stringify(fn(settings())), 49 | }) 50 | } 51 | 52 | const openThemesFolder = () => { 53 | invoke('open_themes') 54 | } 55 | 56 | const appendTheme = async (last: string, theme: string) => { 57 | if (!theme) return 58 | 59 | if (theme === '' || theme === 'none') { 60 | // Remove the previous entry this used to be 61 | setSettings(p => { 62 | return { 63 | ...p, 64 | themes: p.themes.filter(t => t !== last), 65 | } 66 | }) 67 | 68 | return 69 | } 70 | 71 | // Add the new entry, remove the last 72 | setSettings(p => { 73 | return { 74 | ...p, 75 | themes: [...p.themes.filter(t => t !== last && t !== theme), theme], 76 | } 77 | }) 78 | } 79 | return ( 80 | <> 81 | Themes 82 | 83 | Theme 84 | 85 | { 86 | settings().themes.map((theme) => ( 87 | { 92 | appendTheme(theme, e.target.value) 93 | reloadThemes() 94 | }} 95 | options={[{ label: 'None', value: 'none' }, ...themes()]} 96 | /> 97 | ) 98 | ) 99 | } 100 | 101 | { 105 | appendTheme('none', e.target.value) 106 | reloadThemes() 107 | }} 108 | placeholder={'Select a theme...'} 109 | options={[...themes()]} 110 | immutable={true} 111 | /> 112 | 113 | 114 | 115 | 116 | installThemeModal((theme: string) => appendTheme('', theme))}>Install Theme From Link 117 | Open Themes Folder 118 | 119 | > 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /plugins/inline-css/components/Window.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './Window.scss' 2 | import Editor from './Editor.jsx' 3 | import { Close } from './Close.jsx' 4 | import { debounce } from '../../../util/debounce.js' 5 | 6 | const { 7 | ui: { injectCss }, 8 | } = shelter 9 | 10 | let injectedCss = false 11 | 12 | // Handle touches and mouse movement 13 | const getClientCoordinates = ({ touches, clientX, clientY }) => { 14 | if (touches) { 15 | return [ 16 | touches[touches.length - 1].clientX, 17 | touches[touches.length - 1].clientY 18 | ] 19 | } 20 | return [clientX, clientY] 21 | } 22 | 23 | const handleDragging = (onDrag: (evt: DragEvent) => void) => { 24 | const drag = debounce((evt: DragEvent) => { 25 | evt.preventDefault() 26 | onDrag(evt) 27 | }, 5) 28 | 29 | const mouseup = () => { 30 | // Remove movement event since we have lifted up 31 | document.removeEventListener('mousemove', drag) 32 | document.removeEventListener('touchmove', drag) 33 | 34 | document.removeEventListener('mouseup', mouseup) 35 | document.removeEventListener('touchend', mouseup) 36 | } 37 | 38 | // Mousemove is added to the document in case the lement can't catch up and the mouse leaves the elemnts zone 39 | document.addEventListener('mousemove', drag) 40 | document.addEventListener('touchmove', drag) 41 | 42 | document.addEventListener('mouseup', mouseup) 43 | document.addEventListener('touchend', mouseup) 44 | } 45 | 46 | export const Window = () => { 47 | // eslint-disable-next-line prefer-const 48 | let ref = null 49 | 50 | if (!injectedCss) { 51 | injectCss(css) 52 | injectedCss = true 53 | } 54 | 55 | const close = () => { 56 | if (ref) ref.remove() 57 | } 58 | 59 | const topbarMouseDown = (evt) => { 60 | evt.preventDefault() 61 | const [oldClientX, oldClientY] = getClientCoordinates(evt) 62 | 63 | // Get the window ancestor 64 | const windowElm = evt.target.closest('.' + classes.window) 65 | 66 | const rect = windowElm.getBoundingClientRect() 67 | const dragOffsetX = oldClientX - rect.left 68 | const dragOffsetY = oldClientY - rect.top 69 | 70 | 71 | handleDragging((evt) => { 72 | const newX = evt.clientX - dragOffsetX 73 | const newY = evt.clientY - dragOffsetY 74 | 75 | windowElm.style.left = `${newX}px` 76 | windowElm.style.top = `${newY}px` 77 | }) 78 | } 79 | 80 | const resizeMouseDown = (evt) => { 81 | evt.preventDefault() 82 | const [oldClientX, oldClientY] = getClientCoordinates(evt) 83 | 84 | // Get the window ancestor 85 | const windowElm = evt.target.closest('.' + classes.window) 86 | const rect = windowElm.getBoundingClientRect() 87 | 88 | handleDragging((evt) => { 89 | const newWidth = rect.width + evt.clientX - oldClientX 90 | const newHeight = rect.height + evt.clientY - oldClientY 91 | 92 | windowElm.style.width = `${newWidth}px` 93 | windowElm.style.height = `${newHeight}px` 94 | }) 95 | } 96 | 97 | return ( 98 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 118 | 119 | 120 | 121 | 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /plugins/plugin-browser/components/Plugins.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './Plugins.scss' 2 | import { PluginCard } from './PluginCard' 3 | import { getAllPlugins } from '../github.js' 4 | import { getPluginsCache } from '../storage.js' 5 | 6 | const { 7 | ui: { 8 | Button, 9 | injectCss, 10 | Header, 11 | HeaderTags, 12 | Text, 13 | Divider, 14 | TextBox, 15 | showToast 16 | }, 17 | solid: { 18 | createSignal, 19 | createEffect, 20 | } 21 | } = shelter 22 | 23 | let injectedCss = false 24 | const debounce = (fn: (...args) => any, ms: number) => { 25 | let timeoutId = null 26 | return (...args) => { 27 | window.clearTimeout(timeoutId) 28 | timeoutId = window.setTimeout(() => { 29 | fn(...args) 30 | }, ms) 31 | } 32 | } 33 | 34 | export function Plugins() { 35 | if (!injectedCss) { 36 | injectCss(css) 37 | injectedCss = true 38 | } 39 | 40 | const [repos, setRepos] = createSignal([]) 41 | const [search, setSearch] = createSignal('') 42 | 43 | createEffect(async () => { 44 | const cache = await getPluginsCache() 45 | if (cache) { 46 | setRepos(cache) 47 | } else { 48 | loadPlugins() 49 | } 50 | }) 51 | 52 | const loadPlugins = async () => { 53 | const plugins = await getAllPlugins().catch((e) => { 54 | console.error(e) 55 | 56 | showToast({ 57 | title: 'Plugin Browser', 58 | content: 'Failed to load plugins, check DevTools for error.', 59 | duration: 5000, 60 | }) 61 | 62 | return [] 63 | }) 64 | 65 | setRepos(plugins) 66 | } 67 | 68 | return ( 69 | <> 70 | Plugins 71 | 72 | 73 | Not seeing your plugin repo? Take a look at how this plugin finds repos! 74 | 75 | 76 | 77 | 78 | 79 | setSearch(v), 100)} 82 | placeholder={'Search...'} 83 | /> 84 | 85 | { 87 | setRepos([]) 88 | loadPlugins() 89 | }} 90 | > 91 | Refresh 92 | 93 | 94 | 95 | 96 | { 97 | repos().length > 0 ? repos().map((repo: RepoInfo) => { 98 | return ( 99 | <> 100 | 101 | 102 | {repo.repo.owner} 103 | 104 | View Repository - {repo.repo.stars} ⭐ 105 | 106 | 107 | 108 | 109 | { 110 | repo.plugins.map((p: string) => { 111 | if (p.toLowerCase().includes('dorion')) return null 112 | if (!p.toLowerCase().includes(search().toLowerCase())) return null 113 | 114 | return ( 115 | 121 | ) 122 | }) 123 | } 124 | 125 | > 126 | ) 127 | }) : ( 128 | 129 | Loading... 130 | 131 | ) 132 | } 133 | > 134 | ) 135 | } -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/ProfilesPage.tsx: -------------------------------------------------------------------------------- 1 | import { invoke, process } from '../../../api/api.js' 2 | import { Dropdown } from '../../../components/Dropdown.jsx' 3 | import { css, classes } from './ProfilesPage.tsx.scss' 4 | 5 | const { 6 | ui: { Header, Button, HeaderTags, TextBox, injectCss, Divider, ButtonColors, ButtonSizes }, 7 | solid: { createSignal, createEffect }, 8 | } = shelter 9 | 10 | let injectedCss = false 11 | 12 | export function ProfilesPage() { 13 | const [profileList, setProfileList] = createSignal([]) 14 | const [profile, setProfile] = createSignal('') 15 | const [internalProfile, setInternalProfile] = createSignal('') 16 | const [newProfile, setNewProfile] = createSignal('') 17 | 18 | if (!injectedCss) { 19 | injectedCss = true 20 | injectCss(css) 21 | } 22 | 23 | createEffect(async () => { 24 | const profiles = await invoke('get_profile_list') 25 | setProfileList(profiles) 26 | 27 | const config = JSON.parse(await invoke('read_config_file')) 28 | setProfile(config.profile || 'default') 29 | setInternalProfile(config.profile || 'default') 30 | }) 31 | 32 | const saveProfile = async () => { 33 | const config = JSON.parse(await invoke('read_config_file')) 34 | 35 | config.profile = profile() 36 | 37 | await invoke('write_config_file', { 38 | contents: JSON.stringify(config), 39 | }) 40 | 41 | // Relaunch 42 | process.relaunch() 43 | } 44 | 45 | const deleteProfile = async () => { 46 | await invoke('delete_profile', { 47 | name: profile(), 48 | }) 49 | 50 | // Remove the profile from the list 51 | setProfileList(profileList().filter((p) => p !== profile())) 52 | 53 | // Set profile back to internal profile 54 | setProfile(internalProfile()) 55 | } 56 | 57 | const createProfile = async () => { 58 | await invoke('create_profile', { 59 | name: newProfile(), 60 | }) 61 | 62 | // if the profile isn't in the state list, add it 63 | if (!profileList().includes(newProfile())) { 64 | setProfileList([...profileList(), newProfile()]) 65 | } 66 | 67 | // Also set it as the select profile in the list 68 | setProfile(newProfile()) 69 | } 70 | 71 | const handleNewProfileChange = (value: any) => { 72 | setNewProfile(value) 73 | } 74 | 75 | return ( 76 | <> 77 | Profiles 78 | { 80 | return { 81 | label: p, 82 | value: p, 83 | } 84 | })} 85 | placeholder={'Select profile...'} 86 | maxVisibleItems={5} 87 | closeOnSelect={true} 88 | onChange={(e) => setProfile(e.target.value)} 89 | selected={profile()} 90 | /> 91 | 92 | Create Profile 93 | 99 | 100 | 105 | Create Profile 106 | 107 | 108 | 109 | 110 | 111 | 115 | Save and Restart 116 | 117 | 118 | 124 | Delete Selected Profile 125 | 126 | 127 | > 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/util/actionMap.ts: -------------------------------------------------------------------------------- 1 | const { 2 | flux: { 3 | stores, 4 | } 5 | } = shelter 6 | 7 | export const keybindActions: KeybindActionsInternal = { 8 | 'UNASSIGNED': {}, 9 | 'TOGGLE_MUTE': { 10 | press: [{ 11 | type: 'AUDIO_TOGGLE_SELF_MUTE', 12 | context: 'default', 13 | syncRemote: true, 14 | skipMuteUnmuteSoundEffect: false 15 | }] 16 | }, 17 | 'TOGGLE_DEAFEN': { 18 | press: [{ 19 | type: 'AUDIO_TOGGLE_SELF_DEAF', 20 | context: 'default', 21 | syncRemote: true 22 | }] 23 | }, 24 | 'TOGGLE_STREAMER_MODE': { 25 | storeValue: { 26 | store: 'StreamerModeStore', 27 | key: 'enabled', 28 | eventKey: 'value', 29 | modify: (event, store) => { 30 | event['value'] = !store['enabled'] 31 | return event 32 | } 33 | }, 34 | press: [{ 35 | type: 'STREAMER_MODE_UPDATE', 36 | key: 'enabled', 37 | }] 38 | }, 39 | 'TOGGLE_VOICE_MODE': { 40 | storeValue: { 41 | store: 'MediaEngineStore', 42 | key: 'getMode', 43 | eventKey: 'mode', 44 | modify: (event, store) => { 45 | event['mode'] = store['getMode']() === 'PUSH_TO_TALK' ? 'VOICE_ACTIVITY' : 'PUSH_TO_TALK' 46 | event['options'] = store['getModeOptions']() || {} 47 | return event 48 | } 49 | }, 50 | press: [{ 51 | type: 'AUDIO_SET_MODE', 52 | context: 'default' 53 | }] 54 | }, 55 | // TODO grab the existing push to talk bind and display it in the keybinds section 56 | 'PUSH_TO_TALK': { 57 | storeValue: { 58 | store: 'UserStore', 59 | key: '', 60 | eventKey: 'userId', 61 | modify: (event, store) => { 62 | if (event.type === 'SPEAKING') event['userId'] = store['getCurrentUser']().id 63 | 64 | if (event.type === 'PUSH_TO_TALK_STATE_CHANGE') { 65 | // We also have to do some manual stuff 66 | // @ts-expect-error i will explode the typescript checker with my mind 67 | stores.MediaEngineStore?.getMediaEngine().eachConnection(c => c.setForceAudioInput(event.isActive, event.isPriority, false)) 68 | } 69 | 70 | return event 71 | } 72 | }, 73 | press: [{ 74 | type: 'SPEAKING', 75 | context: 'default', 76 | speakingFlags: 1 77 | }, 78 | { 79 | type: 'PUSH_TO_TALK_STATE_CHANGE', 80 | isActive: true, 81 | isPriority: false, 82 | isLatched: true, 83 | }], 84 | release: [{ 85 | type: 'SPEAKING', 86 | context: 'default', 87 | speakingFlags: 0 88 | }, 89 | { 90 | type: 'PUSH_TO_TALK_STATE_CHANGE', 91 | isActive: false, 92 | isPriority: false, 93 | isLatched: false, 94 | }] 95 | }, 96 | 'PUSH_TO_TALK_PRIORITY': { 97 | storeValue: { 98 | store: 'UserStore', 99 | key: '', 100 | eventKey: 'userId', 101 | modify: (event, store) => { 102 | event['userId'] = store['getCurrentUser']().id 103 | return event 104 | } 105 | }, 106 | press: [{ 107 | type: 'SPEAKING', 108 | context: 'default', 109 | speakingFlags: 4 110 | }, 111 | { 112 | type: 'PUSH_TO_TALK_STATE_CHANGE', 113 | isActive: true, 114 | isPriority: true, 115 | isLatched: true, 116 | }], 117 | release: [{ 118 | type: 'SPEAKING', 119 | context: 'default', 120 | speakingFlags: 0 121 | }, 122 | { 123 | type: 'PUSH_TO_TALK_STATE_CHANGE', 124 | isActive: false, 125 | isPriority: false, 126 | isLatched: false, 127 | }] 128 | }, 129 | 'PUSH_TO_MUTE': { 130 | press: [{ 131 | type: 'AUDIO_TOGGLE_SELF_MUTE', 132 | context: 'default', 133 | syncRemote: true, 134 | skipMuteUnmuteSoundEffect: true 135 | }], 136 | release: [{ 137 | type: 'AUDIO_TOGGLE_SELF_MUTE', 138 | context: 'default', 139 | syncRemote: true, 140 | skipMuteUnmuteSoundEffect: true 141 | }] 142 | }, 143 | } 144 | -------------------------------------------------------------------------------- /plugins/dorion-settings/pages/ChangelogPage.tsx: -------------------------------------------------------------------------------- 1 | import { invoke,app, appName } from '../../../api/api.js' 2 | import type { IRelease, TReleases } from '../types/release.js' 3 | import { css, classes } from './ChangelogPage.tsx.scss' 4 | import { processReleaseBodies, loadChangelog, fixImageLinks } from '../util/changelog.js' 5 | 6 | const PAGE_ID = `${appName.toLowerCase()}-changelog-tab` 7 | 8 | const { 9 | ui: { 10 | injectCss, 11 | Header, 12 | HeaderTags, 13 | Button, 14 | ButtonSizes, 15 | ButtonColors, 16 | Text, 17 | LinkButton 18 | }, 19 | solid: { createSignal, createEffect }, 20 | } = shelter 21 | 22 | let injectedCss = false 23 | 24 | export function ChangelogPage() { 25 | if (!injectedCss) { 26 | injectedCss = true 27 | injectCss(css) 28 | } 29 | 30 | const [loading, setLoading] = createSignal(true) 31 | const [releases, setReleases] = createSignal([]) 32 | const [currentVersion, setCurrentVersion] = createSignal('') 33 | const [latestVersion, setLatestVersion] = createSignal('') 34 | const [updateCheck, setUpdateCheck] = createSignal([]) 35 | 36 | createEffect(async () => { 37 | // Load changelog from GitHub 38 | setReleases(await processReleaseBodies(await loadChangelog())) 39 | 40 | // Set current version 41 | setCurrentVersion(`v${await app.getVersion()}`) 42 | 43 | // Set latest version 44 | if (releases().length > 0) { 45 | setLatestVersion(releases()[0].tag_name) 46 | } 47 | 48 | // Check for updates 49 | setUpdateCheck(await invoke('update_check')) 50 | 51 | // Done loading except for images 52 | // Show page as soon as possible to prevent loading page for a long time 53 | setLoading(false) 54 | 55 | // Fix image links 56 | await fixImageLinks(document.getElementById(PAGE_ID)) 57 | }, []) 58 | 59 | async function doUpdate() { 60 | invoke('do_update', { 61 | toUpdate: updateCheck() 62 | }) 63 | } 64 | 65 | async function refresh(): Promise { 66 | setLoading(true) 67 | setReleases(await processReleaseBodies(await loadChangelog())) 68 | setLoading(false) 69 | await fixImageLinks(document.getElementById(PAGE_ID)) 70 | } 71 | 72 | return ( 73 | <> 74 | Changelog 75 | Refresh 76 | {loading() ? ( 77 | 78 | 79 | 80 | ) : ( 81 | <> 82 | {updateCheck().includes('dorion') && ( 83 | 84 | Update available! 85 | Your current version is {currentVersion()} 86 | Update to {latestVersion()} 87 | 88 | )} 89 | {releases() != null && releases().length > 0 && releases().map((release: IRelease) => ( 90 | 91 | 92 | 93 | {release.name} 94 | 95 | 96 | {currentVersion() == release.tag_name && 97 | Current} 98 | {releases()[0].tag_name == release.tag_name && 99 | Latest} 100 | 101 | 102 | View on GitHub 103 | 104 | 105 | ))} 106 | > 107 | )} 108 | > 109 | ) 110 | } 111 | 112 | -------------------------------------------------------------------------------- /plugins/clean-home/index.tsx: -------------------------------------------------------------------------------- 1 | const { 2 | plugin: { 3 | store 4 | }, 5 | ui: { 6 | SwitchItem 7 | } 8 | } = shelter 9 | 10 | const components = [ 11 | { 12 | name: 'Nitro usernames', 13 | description: 'Removes the fancy username effects from users that have them enabled', 14 | rules: ` 15 | div:has(> span[data-username-with-effects]) { all: unset !important; } 16 | span[data-username-with-effects] { all: unset !important; } 17 | span[class*=dnsFont] { all: unset !important; } 18 | ` 19 | }, 20 | { 21 | name: 'Nitro member backgrounds', 22 | description: 'Removes the member list background effects from users that have them enabled', 23 | rules: ` 24 | div[class*=nameplated__] > div[class*=container] { display: none !important; } 25 | ` 26 | }, 27 | { 28 | name: 'Nitro profile effects', 29 | description: 'Removes the profile card effects from users that have them enabled', 30 | rules: ` 31 | div[class*=profileEffects__] { display: none !important; } 32 | ` 33 | }, 34 | { 35 | name: 'Active Now section', 36 | description: 'Removes the "Active Now" section from the home page', 37 | rules: ` 38 | div[class*="nowPlayingColumn"] { display: none; } 39 | ` 40 | }, 41 | { 42 | name: 'Nitro tab', 43 | description: 'Removes the "Nitro" tab from the home page', 44 | rules: ` 45 | a[href="/store"] { display: none; } 46 | ` 47 | }, 48 | { 49 | name: 'Store tab', 50 | description: 'Removes the "Store" tab from the home page', 51 | rules: ` 52 | a[href="/shop"] { display: none; } 53 | ` 54 | }, 55 | { 56 | name: 'Quests tab', 57 | description: 'Removes the "Quests" tab from the home page', 58 | rules: ` 59 | a[href="/quest-home"] { display: none; } 60 | ` 61 | }, 62 | { 63 | name: 'Apps button', 64 | description: 'Removes the Apps button from the text area', 65 | rules: ` 66 | div[class*="channelAppLauncher"] { display: none; } 67 | ` 68 | }, 69 | { 70 | name: 'Quest popout', 71 | description: 'Removes the Nitro quest popup', 72 | rules: ` 73 | div[class*="questPromoContent"] { display: none; } 74 | ` 75 | }, 76 | { 77 | name: 'Server boost bar', 78 | description: 'Removes the server boost barn', 79 | rules: ` 80 | div[data-list-item-id^="channels___boosts-"] { display: none; } 81 | ` 82 | }, 83 | { 84 | name: 'Gift button', 85 | description: 'Removes the gift button in the chat bar', 86 | rules: ` 87 | div[class*="sansAttachButton"] > div[class^="buttons__"] > button { 88 | display: none; 89 | } 90 | ` 91 | } 92 | ] 93 | 94 | const style = document.createElement('style') 95 | style.id = 'clean-home-style' 96 | 97 | const styleElm = document.body.appendChild(style) 98 | 99 | const setStyle = () => { 100 | styleElm.textContent = components 101 | .filter(c => store[c.name]) 102 | .map(c => c.rules) 103 | .join(' ') 104 | } 105 | 106 | // Initial call when we load 107 | setStyle() 108 | 109 | // If this is the first time it's ever been loaded, enable everything and send a toast 110 | if (Object.keys(store).length === 0) { 111 | components.forEach(c => { 112 | store[c.name] = true 113 | }) 114 | setStyle() 115 | 116 | shelter.ui.showToast({ 117 | title: 'Declutter', 118 | content: 'All component removals have been enabled. Click the settings icon to disable them selectively.', 119 | duration: 5000, 120 | }) 121 | } 122 | 123 | export const settings = () => { 124 | return ( 125 | 126 | {components.map(c => ( 127 | { 130 | store[c.name] = value 131 | setStyle() 132 | }} 133 | note={c.description} 134 | > 135 | Remove {c.name} 136 | 137 | ))} 138 | 139 | ) 140 | 141 | } 142 | 143 | export const onUnload = () => { 144 | styleElm.remove() 145 | } 146 | -------------------------------------------------------------------------------- /plugins/dorion-titlebar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Controls, Titlebar } from './Titlebar.jsx' 2 | import { setMaximizeIcon } from './actions.js' 3 | import { css, classes } from './index.scss' 4 | import { disobserve, waitForElm } from './waitElm.js' 5 | 6 | const { 7 | ui: { injectCss }, 8 | flux: { dispatcher } 9 | } = shelter 10 | 11 | let injectedCss = false 12 | 13 | const insertOne = (classNames: Array | string, callbackFn: () => void) => { 14 | if (!Array.isArray(classNames)) classNames = [classNames] 15 | classNames.forEach(className => { 16 | document.querySelectorAll(`div.${className}`).forEach(e => { 17 | e.remove() 18 | }) 19 | }) 20 | callbackFn() 21 | } 22 | 23 | const insertTitleBar = (parent: Element) => { 24 | insertOne(classes.dorion_topbar, () => parent.prepend()) 25 | } 26 | 27 | const insertStandaloneControl = (parent: Element) => { 28 | insertOne([classes.dorion_topbar, classes.topright], () => parent.appendChild()) 29 | setMaximizeIcon() 30 | } 31 | 32 | const waitDiscordPanel = (callbackFn: (elm: Element) => void) => waitForElm(['>div#app-mount', '>div[class*=appAsidePanelWrapper]', '>div[class*=notAppAsidePanel]'], {callbackFn}) 33 | 34 | // if titlebar injected at `document.body`, `div#app-mount`, `div[class*=appAsidePanelWrapper]`, `div[class*=notAppAsidePanel]` 35 | // would be worst case with overflow or some contents covered causing some parts Discord cannot be seen 36 | const injectControls = async () => { 37 | // always keep a title bar available if following elms not available 38 | insertTitleBar(document.body) 39 | // cancel old observer to inject new controls 40 | const discordPanel = await waitDiscordPanel(elm => insertTitleBar(elm)) 41 | const discordBar = await waitForElm(['div[data-layer=base]>div[class*=container]', '>div[class*=base]', ['>div[class*=bar_]', '>div[class*=-bar]']], {root:discordPanel}) 42 | waitForElm('>div[class*=trailing]', {callbackFn: elm => { 43 | insertStandaloneControl(elm) 44 | }, root: discordBar}) 45 | waitForElm('>div[class*=title]', {callbackFn: elm => { 46 | elm.setAttribute('data-tauri-drag-region', 'true') 47 | }, root: discordBar}) 48 | } 49 | 50 | const handleFullTitlebar = async () => { 51 | // cancel old observer to inject new titlebar 52 | waitDiscordPanel(elm => insertTitleBar(elm)) 53 | } 54 | 55 | const handleControlsOnly = async () => { 56 | // use querySelector, do nothing while observer still injecting elms 57 | const dorionControl = document.querySelector(`div[class*=notAppAsidePanel] div[data-layer=base][class*=baseLayer] div[class*=base]>div[class*=bar]>div[class*=trailing] div.${classes.topright}`) 58 | if (dorionControl) document.querySelectorAll(`.${classes.dorion_topbar}`)?.forEach(e => e.remove()) 59 | } 60 | 61 | const handleFullscreenExit = dispatch => { 62 | if (dispatch.isElementFullscreen) return 63 | 64 | injectControls() 65 | } 66 | 67 | export const onLoad = async () => { 68 | // @ts-expect-error shut up 69 | if (window?.__DORION_CONFIG__?.use_native_titlebar || await window?.__TAURI__?.core.invoke('get_platform') === 'macos') return 70 | 71 | if (!injectedCss) { 72 | injectCss(css) 73 | injectedCss = true 74 | } 75 | 76 | // @ts-expect-error shut up 77 | window?.__TAURI__?.event.listen( 78 | // @ts-expect-error shut up 79 | window.__TAURI__.event.TauriEvent.WINDOW_RESIZED, 80 | setMaximizeIcon 81 | ) 82 | 83 | // @ts-expect-error shut up 84 | window?.__TAURI__?.core.invoke('remove_top_bar') 85 | 86 | injectControls() 87 | 88 | dispatcher.subscribe('LAYER_PUSH', handleFullTitlebar) 89 | dispatcher.subscribe('LAYER_POP', handleControlsOnly) 90 | dispatcher.subscribe('LOGIN_SUCCESS', injectControls) 91 | dispatcher.subscribe('WINDOW_FULLSCREEN_CHANGE', handleFullscreenExit) 92 | } 93 | 94 | export const onUnload = () => { 95 | disobserve() 96 | dispatcher.unsubscribe('LAYER_PUSH', handleFullTitlebar) 97 | dispatcher.unsubscribe('LAYER_POP', handleControlsOnly) 98 | dispatcher.unsubscribe('LOGIN_SUCCESS', injectControls) 99 | dispatcher.unsubscribe('WINDOW_FULLSCREEN_CHANGE', handleFullscreenExit) 100 | } 101 | -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/components/KeybindSection.tsx: -------------------------------------------------------------------------------- 1 | import { css, classes } from './KeybindSection.tsx.scss' 2 | import { Dropdown } from '../../../components/Dropdown' 3 | import { KeybindInput } from '../../../components/KeybindInput' 4 | 5 | const { 6 | ui: { 7 | Text, 8 | HeaderTags, 9 | Header, 10 | injectCss 11 | }, 12 | solid: { 13 | createSignal 14 | } 15 | } = shelter 16 | 17 | interface Props { 18 | internalName?: string 19 | enabled?: boolean 20 | 21 | keybindActionTypes: KeybindActionType[] 22 | keybindDescriptions: KeybindDescription[] 23 | keybind?: Keybind 24 | 25 | onKeybindChange: (keybind: Keybind, old: Keybind) => void 26 | onKeybindRemove: (keybind: Keybind) => void 27 | 28 | // Not to be confused with keybind related stuff, this is just so it can be used in a loop 29 | key?: any 30 | } 31 | 32 | interface CLoseProps { 33 | onClick: () => void 34 | } 35 | 36 | const RemoveIcon = (props: CLoseProps) => ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | ) 44 | let injectedCss = false 45 | 46 | export function KeybindSection(props: Props) { 47 | if (!injectedCss) { 48 | injectedCss = true 49 | injectCss(css) 50 | } 51 | 52 | const [keybindType, setKeybindType] = createSignal(props.internalName || props.keybind?.key || props.keybindActionTypes[0].value) 53 | const old = props.keybind 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | Action 61 | 62 | 63 | { 67 | setKeybindType(e.target.value) 68 | props.onKeybindChange({ 69 | keys: props.keybind.keys || [], 70 | key: e.target.value, 71 | }, old) 72 | }} 73 | style='width: 90%' 74 | > 75 | 76 | 77 | 78 | 79 | Keybind 80 | 81 | 82 | { 85 | props.onKeybindChange({ 86 | keys: keybind, 87 | key: keybindType(), 88 | }, old) 89 | }} 90 | style='width: 100%' 91 | /> 92 | 93 | 94 | 95 | props.onKeybindRemove(old)} /> 96 | 97 | 98 | 99 | 100 | {props.keybindDescriptions[keybindType()]} 101 | 102 | 103 | 104 | ) 105 | } -------------------------------------------------------------------------------- /plugins/dorion-notifications/index.tsx: -------------------------------------------------------------------------------- 1 | import { api, invoke } from '../../api/api.js' 2 | 3 | const { 4 | ui: { 5 | SwitchItem, 6 | ReactiveRoot 7 | }, 8 | flux: { 9 | dispatcher: FluxDispatcher 10 | }, 11 | solid: { 12 | createSignal, 13 | }, 14 | observeDom, 15 | } = shelter 16 | 17 | const [settings, setSettings] = createSignal(null) 18 | 19 | let child: Element = null 20 | 21 | const settingsHandler = (payload) => { 22 | if (payload.section !== 'Notifications') { 23 | if (child) { 24 | child.remove() 25 | child = null 26 | } 27 | return 28 | } 29 | 30 | const unsub = observeDom('#notifications-tab', () => { 31 | unsub() 32 | 33 | const notifSelector = 'div[class*="contentColumn"] div[class*="container"]' 34 | const node = document.querySelector(notifSelector) as HTMLElement 35 | 36 | if (!node) return 37 | 38 | // Hide the original notification settings 39 | node.style.display = 'none' 40 | 41 | // The next node after should also be hidden 42 | const next = node.nextElementSibling as HTMLDivElement 43 | if (next) next.style.display = 'none' 44 | 45 | const NotificationSettings = () => [ 46 | { 50 | setSettings({ 51 | ...settings(), 52 | desktop_notifications: value 53 | }) 54 | 55 | // If enabling, dispatch the flux event as well 56 | FluxDispatcher.dispatch({ 57 | type: 'NOTIFICATIONS_SET_PERMISSION_STATE', 58 | enabled: value ? 'ENABLED' : 'DISABLED' 59 | }) 60 | 61 | invoke('write_config_file', { 62 | contents: JSON.stringify(settings()) 63 | }) 64 | 65 | if (value) { 66 | invoke('send_notification', { 67 | title: 'Desktop Notifications Enabled', 68 | body: 'You will now receive desktop notifications!', 69 | icon: '', 70 | }) 71 | } 72 | }} 73 | > 74 | Enable Desktop Notifications 75 | , 76 | { 80 | setSettings({ 81 | ...settings(), 82 | unread_badge: value 83 | }) 84 | 85 | await invoke('write_config_file', { 86 | contents: JSON.stringify(settings()) 87 | }) 88 | 89 | api.shouldShowUnreadBadge = value 90 | 91 | // Also wipe the current badge if it was enabled 92 | if (!value) invoke('notif_count', { amount: 0 }) 93 | else api.util.applyNotificationCount() 94 | }} 95 | > 96 | Enable Unread Message Badge 97 | , 98 | ] 99 | 100 | child = node.parentElement.insertBefore( 101 | 102 | 103 | , 104 | node.parentElement.firstChild 105 | ) 106 | }) 107 | } 108 | 109 | const notifHandler = (payload) => { 110 | // @ts-expect-error this is added by Dorion 111 | if (!settings()?.desktop_notifications || !window.Notification?.__IS_STUBBED__) return 112 | 113 | const { title, body, icon, message } = payload 114 | 115 | invoke('send_notification', { 116 | title, 117 | body, 118 | icon, 119 | additionalData: { 120 | guild_id: message?.guild_id || null, 121 | channel_id: message?.channel_id || null, 122 | message_id: message?.id || null, 123 | }, 124 | }) 125 | } 126 | 127 | FluxDispatcher.subscribe('USER_SETTINGS_MODAL_SET_SECTION', settingsHandler) 128 | FluxDispatcher.subscribe('RPC_NOTIFICATION_CREATE', notifHandler) 129 | 130 | export const onLoad = async () => { 131 | const cfg = JSON.parse(await invoke('read_config_file')) 132 | setSettings(cfg) 133 | 134 | if (cfg.desktop_notifications) { 135 | Notification.requestPermission() 136 | } 137 | } 138 | 139 | export const onUnload = () => { 140 | FluxDispatcher.unsubscribe('USER_SETTINGS_MODAL_SET_SECTION', settingsHandler) 141 | FluxDispatcher.unsubscribe('RPC_NOTIFICATION_CREATE', notifHandler) 142 | } 143 | -------------------------------------------------------------------------------- /plugins/plugin-browser/github.ts: -------------------------------------------------------------------------------- 1 | import { getPluginJsonCache, savePluginJsonCache, savePluginsCache } from './storage.js' 2 | 3 | const ghFetch = async (url: string) => { 4 | return fetch(url, { 5 | headers: { 6 | 'User-Agent': 'Shelter Plugin Browser', 7 | } 8 | }) 9 | } 10 | 11 | export async function getRepos() { 12 | const resp = await ghFetch('https://api.github.com/search/repositories?q=shelter-plugins') 13 | const json = await resp.json() 14 | 15 | // Transform into more digestable/saveable data 16 | return json.items.map((item: any) => { 17 | return { 18 | name: item.name, 19 | description: item.description, 20 | url: item.html_url, 21 | stars: item.stargazers_count, 22 | owner: item.owner.login, 23 | owner_url: item.owner.html_url, 24 | owner_avatar: item.owner.avatar_url, 25 | homepage: item.homepage, 26 | } 27 | }) satisfies Repo[] 28 | } 29 | 30 | export async function pluginsSite(repo: Repo) { 31 | // If there is a linked website in the repo, use that, otherwise assume .github.io/ 32 | if (repo.homepage) { 33 | return repo.homepage 34 | } else { 35 | return `https://${repo.owner}.github.io/${repo.name}` 36 | } 37 | } 38 | 39 | export async function getRepoPlugins(repo: Repo) { 40 | // List all the files in the plugins/ directory in the repo 41 | const resp = await ghFetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/contents/plugins`) 42 | const json = await resp.json() 43 | 44 | // Ensure this is an array 45 | if (!Array.isArray(json)) { 46 | return [] 47 | } 48 | 49 | return json.map((item: any) => item.name) 50 | } 51 | 52 | export async function getPluginsLocation(site: string, plugins: string[]) { 53 | // People could be putting their plugins anywhere, but two common places are: 54 | // // OR at the root, / 55 | // we can test for these simply by trying to get //plugin.json and seeing if it exists 56 | const plugin = plugins?.[0] 57 | 58 | if (!plugin) { 59 | return site 60 | } 61 | 62 | const paths = [ 63 | `${site}/shelter-plugins/`, 64 | `${site}/`, 65 | ] 66 | let workingPath: null | string = site 67 | 68 | for (const path of paths) { 69 | const url = `${path}/${plugin}/plugin.json` 70 | const resp = await ghFetch(url) 71 | 72 | try { 73 | const json = await resp.json() 74 | if (json.name) { 75 | workingPath = path 76 | break 77 | } 78 | 79 | // eslint-disable-next-line 80 | } catch (e) { 81 | // If we get an error, its probably because the file doesn't exist 82 | // So we can just ignore it 83 | } 84 | } 85 | 86 | return workingPath 87 | } 88 | 89 | export async function getPluginJson(site: string, plugin: string) { 90 | const url = `${site}/${plugin}/plugin.json` 91 | 92 | // Check if we have it cached 93 | const cache = getPluginJsonCache() 94 | if (cache[url]) { 95 | return cache[url] 96 | } 97 | 98 | const resp = await ghFetch(url) 99 | 100 | try { 101 | const json = await resp.json() 102 | 103 | // Cache the plugin.json 104 | savePluginJsonCache(url, json) 105 | 106 | return json 107 | } catch (e) { 108 | console.log('[Plugin Browser] Error parsing plugin.json: ', e.message) 109 | return null 110 | } 111 | } 112 | 113 | export async function getAllPlugins() { 114 | const repos = await getRepos() 115 | 116 | // Map the plugins to their repos 117 | let plugins = await Promise.all(repos.map(async (repo) => { 118 | try { 119 | const site = await pluginsSite(repo) 120 | 121 | if (!site) { 122 | console.log('[Plugin Browser] No site found for repo: ', repo.name) 123 | return null 124 | } 125 | 126 | const plugins = await getRepoPlugins(repo) 127 | 128 | if (!plugins || plugins.length === 0) { 129 | console.log(`[Plugin Browser] No plugins found for repo: ${repo.owner}/${repo.name}`) 130 | return null 131 | } 132 | 133 | return { 134 | site: await getPluginsLocation(site, plugins), 135 | repo, 136 | plugins, 137 | } satisfies RepoInfo 138 | } catch(e) { 139 | console.error(e) 140 | return null 141 | } 142 | })) 143 | 144 | plugins = plugins.filter((plugin) => plugin !== null) 145 | 146 | // Save to cache 147 | savePluginsCache(plugins) 148 | 149 | return plugins as RepoInfo[] 150 | } -------------------------------------------------------------------------------- /plugins/shelteRPC/components/GameCard.tsx: -------------------------------------------------------------------------------- 1 | import { backend, event } from '../../../api/api.js' 2 | import { timestampToRelative } from '../util.js' 3 | import { css, classes } from './GameCard.scss' 4 | 5 | interface Props { 6 | name?: string 7 | manuallyAdded?: boolean 8 | lastPlayed?: number 9 | type: 'playing' | 'played' | 'none' 10 | local: boolean 11 | } 12 | 13 | const { 14 | ui: { 15 | injectCss 16 | }, 17 | plugin: { 18 | store 19 | }, 20 | solid: { 21 | createSignal 22 | } 23 | } = shelter 24 | 25 | const trashIcon = () => ( 26 | 27 | 28 | 29 | 30 | ) 31 | 32 | // https://iconmonstr.com/eye-9-svg/ 33 | const hideIcon = () => ( 34 | 35 | 36 | 37 | ) 38 | 39 | // https://iconmonstr.com/eye-10-svg/ 40 | const hideClosed = () => ( 41 | 42 | 43 | 44 | ) 45 | 46 | let injectedCss = false 47 | 48 | const deleteGame = (name: string) => { 49 | // Remove from local detectables 50 | if (backend !== 'None') { 51 | event.emit('remove_detectable', { 52 | name, 53 | exe: '' 54 | }) 55 | } 56 | 57 | // Also remove from the plugin store 58 | const key = Object.keys(store.previouslyPlayed).find(k => store.previouslyPlayed[k].name === name) 59 | delete store.previouslyPlayed[key] 60 | 61 | // If the currently playing game is the one we're deleting, set it to nothing 62 | if (store.currentlyPlaying === name) { 63 | store.currentlyPlaying = '' 64 | } 65 | } 66 | 67 | export default (props: Props) => { 68 | if (!injectedCss) { 69 | injectedCss = true 70 | injectCss(css) 71 | } 72 | 73 | const [hide, setHide] = createSignal(props.name ? store.previouslyPlayed[props.name]?.hide : false) 74 | 75 | return ( 76 | 81 | 82 | {props.name || 'No game detected'} 83 | 84 | { 85 | props.type === 'played' ? <>Last played: {timestampToRelative(props.lastPlayed)}> : 86 | props.type === 'playing' && props.name ? 'Now playing!' : 87 | 'What are you playing?' 88 | } 89 | 90 | 91 | 92 | 93 | { props.local && { 94 | deleteGame(props.name || '') 95 | }}>{trashIcon()} } 96 | 97 | { (props.name && props.type !== 'playing') && { 98 | if (!props.name) return 99 | 100 | // Toggle hiding the game via store.previouslyPlayed 101 | const key = Object.keys(store.previouslyPlayed).find(k => store.previouslyPlayed[k].name === props.name) 102 | store.previouslyPlayed[key].hide = !hide() 103 | 104 | setHide(!hide()) 105 | }}> 106 | { hide() ? hideClosed() : hideIcon() } 107 | } 108 | 109 | 110 | ) 111 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | SpikeHD's shelter plugins 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Shelter plugin repo housing both general-use plugins AND Dorion-specific plugins 13 | 14 | 15 | 16 | 17 | # Table of Contents 18 | * [Plugins](#plugins) 19 | * [Always Trust](#always-trust) 20 | * [Blur NSFW](#blur-nsfw) 21 | * [Declutter](#declutter) 22 | * [Inline CSS](#inline-css) 23 | * [Invisible Typing](#invisible-typing) 24 | * [No Reply Mention](#no-reply-mention) 25 | * [OpenAsar DOM Optimizer](#openasar-dom-optimizer) 26 | * [Orbolay Bridge](#orbolay-bridge) 27 | * [Platform Spoofer](#platform-spoofer) 28 | * [Plugin Browser](#plugin-browser) 29 | * [shelteRPC](#shelterpc) 30 | * [UserPFP](#userpfp) 31 | * [Web keybinds](#web-keybinds) 32 | * [You're Right](#youre-right) 33 | * [Dorion Plugins](#dorion-plugins) 34 | 35 | # Plugins 36 | 37 | ## Always Trust 38 | 39 | Remove the "You are leaving Discord" popup. 40 | 41 | `https://spikehd.dev/shelter-plugins/always-trust/` 42 | 43 | ## Blur NSFW 44 | 45 | Blur images and videos in NSFW channels. Hover to unblur, the click-preview is also automatically unblurred. 46 | 47 | `https://spikehd.dev/shelter-plugins/blur-nsfw/` 48 | 49 |  50 | 51 | ## Declutter 52 | 53 | Hide/disable unwanted or distracting components, such as Nitro effects, Store/Nitro tabs, and much more. 54 | 55 | `https://spikehd.dev/shelter-plugins/clean-home` 56 | 57 | ## Inline CSS 58 | 59 | Lightweight inline CSS editor, with hot reloading. 60 | 61 | `https://spikehd.dev/shelter-plugins/inline-css/` 62 | 63 |  64 | 65 | ## Invisible Typing 66 | 67 | Prevents others from seeing when you are typing. 68 | 69 | `https://spikehd.dev/shelter-plugins/invisible-typing/` 70 | 71 |  72 | 73 | ## No Reply Mention 74 | 75 | Disables mentions on replies by default. 76 | 77 | `https://spikehd.dev/shelter-plugins/no-reply-mention/` 78 | 79 | ## OpenAsar DOM Optimizer 80 | 81 | Port of a neat DOM optimization trick [from OpenAsar](https://github.com/GooseMod/OpenAsar/blob/ef4470849624032a8eb7265eabd23158aa5a2356/src/mainWindow.js#L99). 82 | See the [OpenAsar wiki](https://github.com/GooseMod/OpenAsar/wiki/DOM-Optimizer) for more information on how this works! 83 | 84 | `https://spikehd.dev/shelter-plugins/openasar-dom-optimizer/` 85 | 86 | ## Orbolay Bridge 87 | 88 | Bridge plugin required to make [Orbolay](https://github.com/SpikeHD/Orbolay) work. 89 | 90 | `https://spikehd.dev/shelter-plugins/orbolay/` 91 | 92 | 93 | 94 | ## Platform Spoofer 95 | 96 | Pretend to be on a different platform. Basically only visible to those using a platform indicator plugin. 97 | 98 | `https://spikehd.dev/shelter-plugins/platform-spoof/` 99 | 100 | ## Plugin Browser 101 | 102 | Find many Shelter plugins in one place! If you're curious why something isn't showing up, view the docs on it. 103 | 104 | `https://spikehd.dev/shelter-plugins/plugin-browser/` 105 | 106 |  107 | 108 | ## shelteRPC 109 | 110 | arRPC replication for Shelter. Only really useful on web Discord, and if you aren't using a client/mod that has it built-in, you'll need to run an [arRPC](https://github.com/OpenAsar/arrpc) or [rsRPC](https://github.com/SpikeHD/rsRPC) server. 111 | 112 | `https://spikehd.dev/shelter-plugins/shelteRPC/` 113 | 114 |  115 | 116 | ## UserPFP 117 | 118 | View and use animated profile pictures without Nitro. See the [UserPFP README](https://github.com/UserPFP/UserPFP) for more information. 119 | 120 | `https://spikehd.dev/shelter-plugins/userpfp/` 121 | 122 | ## Web Keybinds 123 | 124 | (Incomplete) port of Vencord's `WebKeybinds` plugin. 125 | 126 | `https://spikehd.dev/shelter-plugins/web-keybinds/` 127 | 128 | ## You're Right 129 | 130 | (Experimental) Float your messages to the right instead of the left. Like a texting app. Or something. 131 | 132 |  133 | 134 | # Dorion Plugins 135 | 136 | **DO NOT INSTALL DORION PLUGINS**, they are for the [Dorion client](https://github.com/SpikeHD/Dorion) only and will not work for you 137 | -------------------------------------------------------------------------------- /plugins/dorion-custom-keybinds/components/Keybinds.tsx: -------------------------------------------------------------------------------- 1 | import { event, invoke, process } from '../../../api/api' 2 | import { css, classes } from './Keybinds.tsx.scss' 3 | import { KeybindSection } from './KeybindSection' 4 | 5 | const { 6 | ui: { 7 | Button, 8 | Text, 9 | SwitchItem, 10 | injectCss 11 | }, 12 | solid: { 13 | createSignal, 14 | createEffect 15 | } 16 | } = shelter 17 | 18 | let injectedCss = false 19 | 20 | interface Props { 21 | keybindActionTypes: KeybindActionType[] 22 | keybindDescriptions: KeybindDescription[] 23 | } 24 | 25 | export function Keybinds(props: Props) { 26 | if (!injectedCss) { 27 | injectedCss = true 28 | injectCss(css) 29 | } 30 | 31 | const [keybindsEnabled, setKeybindsEnabled] = createSignal(false) 32 | const [keybindEnabledChanged, setKeybindEnabledChanged] = createSignal(false) 33 | // list of keybinds that are set (aka keybinds that have a section already) 34 | const [keybindSections, setKeybindSections] = createSignal([]) 35 | 36 | createEffect(async () => { 37 | const keybinds = await invoke('get_keybinds') 38 | const config = await invoke('get_config') 39 | 40 | // Convert the map (key: bind[]) to the array 41 | const sections = Object.keys(keybinds).map((key) => ({ 42 | key, 43 | keys: keybinds[key] 44 | })) 45 | 46 | setKeybindSections(sections) 47 | setKeybindsEnabled(config.keybinds_enabled ?? false) 48 | }, []) 49 | 50 | const updateKeybinds = (keybinds: Keybind[]) => { 51 | setKeybindSections(keybinds) 52 | 53 | event.emit('keybinds_changed', keybinds) 54 | } 55 | 56 | return ( 57 | 58 | { 59 | keybindEnabledChanged() && ( 60 | 61 | 62 | Enabling or disabling global keybinds requires a restart to take effect. 63 | 64 | 65 | { 69 | process.relaunch() 70 | }} 71 | > 72 | Restart 73 | 74 | 75 | ) 76 | } 77 | 78 | 79 | 80 | 81 | Global keybinds are an experimental feature! 82 | 83 | 84 | 85 | { 89 | // Ensure keybinds list max is the same as the keybindActionTypes list 90 | if (keybindSections().length >= props.keybindActionTypes.length) { 91 | return 92 | } 93 | 94 | updateKeybinds([...keybindSections(), { 95 | key: 'UNASSIGNED', 96 | keys: [] 97 | }]) 98 | }} 99 | > 100 | Add Keybind 101 | 102 | 103 | 104 | 105 | { 108 | setKeybindsEnabled(value) 109 | setKeybindEnabledChanged(true) 110 | invoke('set_config', { 111 | config: { 112 | ...await invoke('get_config'), 113 | keybinds_enabled: value 114 | } 115 | }) 116 | }} 117 | note="Enable or disable global keybinds. Requires restart." 118 | > 119 | Enable Global Keybinds 120 | 121 | 122 | 123 | { 124 | keybindSections().map((section: Keybind, idx) => ( 125 | { 130 | if (section.key === 'UNASSIGNED' || section.key === type.value) return true 131 | return !keybindSections().some((keybind) => keybind.key === type.value) 132 | }) 133 | } 134 | keybindDescriptions={props.keybindDescriptions} 135 | keybind={section} 136 | onKeybindChange={(keybind, old) => { 137 | // If the keybind is the same (and we are just changing the action), we don't need to remove the old keybind, 138 | // we can just modify the existing keybind 139 | if (keybind.key === old.key) { 140 | updateKeybinds(keybindSections().map((bind) => { 141 | if (bind.key === keybind.key) { 142 | return keybind 143 | } 144 | 145 | return bind 146 | })) 147 | 148 | return 149 | } 150 | 151 | const newKeybinds = keybindSections().filter( 152 | bind => bind.key !== keybind.key && bind.key !== old.key 153 | ) 154 | 155 | newKeybinds.push(keybind) 156 | 157 | updateKeybinds(newKeybinds) 158 | }} 159 | onKeybindRemove={(keybind) => { 160 | updateKeybinds(keybindSections().filter((bind) => bind.key !== keybind.key)) 161 | }} 162 | /> 163 | )) 164 | } 165 | 166 | ) 167 | } 168 | --------------------------------------------------------------------------------