Compare commits

..

No commits in common. "642533f8007a37067d612c03d8f3871c9d52b68a" and "99281f3f0570cd91b3defeb1cd482c9ca439f27f" have entirely different histories.

61 changed files with 885 additions and 3221 deletions

View file

@ -23,13 +23,6 @@
- [ ] Manual verification for affected flows
- [ ] Screenshots or recordings for UI changes
## AI Notice
<!-- Please answer truthfully. AI/LLM tools include programs like Claude, Codex, and Cursor -->
- [ ] I have used any AI/LLM tool to generate code or text in this PR. I understand that I am responsible for reviewing and validating any code generated by AI, and that I will be held accountable for any issues caused by this code.
- [ ] I have not used any AI to generate code in this PR.
## Notes
<!-- Add links to related issues, screenshots, follow-up work, or implementation details. -->

18
.vscode/launch.json vendored
View file

@ -1,18 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"args": ["."],
"outputCapture": "std",
"outFiles": ["${workspaceFolder}/**/*.js"]
}
]
}

View file

@ -98,11 +98,6 @@ Before requesting review, verify:
- Reference related issues and prior discussions.
- If you added a workaround/suppression, include the reason in code comments and in the PR description.
## AI Notice
While we welcome AI-assisted development, we expect contributors to review and validate any code generated by AI tools. You are responsible for ensuring that any AI-generated code meets our quality, security, and performance standards.
We don't allow vibecoded slop. There has to be at least slight human touch in the code you submit. If you use AI tools to generate code, please disclose it in your PR description. Don't lie about it. We can tell and we will not accept PRs that are entirely AI-generated.
## Documentation
We are still improving project and codebase documentation.

View file

@ -1,52 +0,0 @@
#legcord-invite-back {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 2147483646;
display: none;
align-items: center;
justify-content: center;
gap: 8px;
margin: 0;
padding: 0 16px;
min-height: 38px;
border: none;
border-radius: 8px;
background: var(--brand-500, #5865f2);
color: var(--white-500, #fff);
font-family: var(--font-primary, "gg sans", "gg sans Fallback", "Noto Sans", Helvetica, Arial, sans-serif);
font-size: 14px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0;
white-space: nowrap;
cursor: pointer;
box-shadow:
var(--elevation-high, 0 8px 16px rgba(0, 0, 0, 0.24)),
0 0 0 1px rgba(0, 0, 0, 0.08);
-webkit-app-region: no-drag;
user-select: none;
transition:
background-color 0.17s ease,
box-shadow 0.17s ease,
transform 0.17s ease;
}
#legcord-invite-back[data-visible="true"] {
display: inline-flex;
}
#legcord-invite-back:hover {
background: var(--brand-560, #4752c4);
}
#legcord-invite-back:active {
background: var(--brand-600, #3c45a5);
transform: translateX(-50%) translateY(1px);
}
#legcord-invite-back:focus-visible {
outline: 2px solid var(--brand-500, #5865f2);
outline-offset: 2px;
}

View file

@ -89,12 +89,7 @@
"settings-prfmMode": "Leistungsmodus",
"settings-prfmMode-desc": "Der Leistungsmodus ist eine experimentelle Funktion in Legcord, die darauf ausgelegt ist, die Reaktionsfähigkeit und Leistung entsprechend Ihren Anforderungen zu optimieren. Die Auswirkungen können je nach Ihrer Hardware und Nutzung variieren. Wir empfehlen Ihnen daher, jeden Modus auszuprobieren, um herauszufinden, welcher für Sie am besten geeignet ist.",
"settings-prfmMode-performance": "Leistung",
"settings-prfmMode-balanced": "Ausgewogen",
"settings-prfmMode-battery": "Batterie",
"settings-prfmMode-memory": "Speichersparer",
"settings-prfmMode-voip": "Sprache & Video",
"settings-prfmMode-latency": "Niedrige Latenz",
"settings-prfmMode-smoothScreenshare": "Flüssige Bildschirmfreigabe",
"settings-prfmMode-dynamic": "Dynamisch",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Automatische Verstärkung deaktivieren",

View file

@ -79,11 +79,11 @@
"settings-hardwareAcceleration": "Hardware acceleration",
"settings-hardwareAcceleration-desc": "Hardware acceleration uses your GPU to make Legcord run faster. If you're experiencing visual glitches, try disabling this.",
"settings-processScanning": "Process scanning",
"settings-processScanning-desc": "Scan for running games to improve Rich Presence detection. Disable to reduce CPU usage. Requires a restart.",
"settings-processScanning-desc": "Enables scanning for running games to improve Rich Presence detection.",
"settings-windowsLegacyScanning": "Windows legacy scanning",
"settings-windowsLegacyScanning-desc": "Uses legacy method for scanning processes on Windows (pre v1.1.6). May improve compatibility on some systems but is less efficient. Requires a restart.",
"settings-windowsLegacyScanning-desc": "Uses legacy method for scanning processes on Windows (pre v1.1.6). May improve compatibility on some systems but is less efficient.",
"settings-scanInterval": "Scan interval (ms)",
"settings-scanInterval-desc": "How often (in milliseconds) process scanning runs. Lower values detect games faster but use more CPU. Minimum 1000. Requires a restart.",
"settings-scanInterval-desc": "Sets how often (in milliseconds) the process scanning occurs. Lower values may improve detection speed but can increase CPU usage.",
"settings-blockPowerSavingInVoiceChat": "Block power saving in voice chat",
"settings-blockPowerSavingInVoiceChat-desc": "Prevent Legcord from being suspended. Keeps system active but allows screen to be turned off.",
"settings-mobileMode": "Mobile mode",
@ -115,12 +115,7 @@
"settings-prfmMode": "Performance mode",
"settings-prfmMode-desc": "Performance Mode is an experimental feature in Legcord designed to optimize responsiveness and performance based on your needs. The impact may vary depending on your hardware and usage, so we encourage you to try each mode to determine which works best for you.",
"settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Balanced",
"settings-prfmMode-battery": "Battery",
"settings-prfmMode-memory": "Memory saver",
"settings-prfmMode-voip": "Voice & video",
"settings-prfmMode-latency": "Low latency",
"settings-prfmMode-smoothScreenshare": "Smooth screenshare",
"settings-prfmMode-dynamic": "Dynamic",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Disable autogain",
@ -147,7 +142,6 @@
"settings-save": "Save Settings",
"settings-experimental": "Experimental",
"settings-restart": "Restart App",
"invite-goBackToApp": "Go back to the Discord app",
"settings-updater": "Check for updates",
"settings-skipSplash": "Skip Splash Screen",
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
@ -162,39 +156,10 @@
"settings-quickCss": "Quick CSS",
"settings-quickCss-desc": "Quickly edit your CSS in a simple text editor. Changes are applied immediately after saving the file.",
"settings-category-lookAndFeel": "Look and feel",
"settings-category-lookAndFeel-desc": "Window chrome, tray icon, splash screen, and mobile layout.",
"settings-category-mods": "Mods",
"settings-category-mods-desc": "Client mods, CSP, and plugin capabilities.",
"settings-category-behaviour": "Behaviour",
"settings-category-behaviour-desc": "Startup, tray, scrolling, spellcheck, and Discord channel.",
"settings-category-legacy": "Legacy features",
"settings-category-networking": "Networking",
"settings-category-networking-desc": "HTTP(S) and SOCKS proxy settings for Discord and downloads.",
"settings-category-debug": "Debug options",
"settings-category-powerManagement": "Power Management",
"settings-category-powerManagement-desc": "Performance presets and power-saving behaviour.",
"settings-category-arrpc": "Rich Presence",
"settings-category-arrpc-desc": "arRPC game activity detection and scanning.",
"settings-category-backup": "Backup and restore",
"settings-category-backup-desc": "Export or restore settings, themes, plugins, and mod data.",
"settings-category-advanced": "Advanced",
"settings-category-advanced-desc": "Audio capture, hardware, updates, flags, and debug tools.",
"settings-search": "Search settings",
"settings-search-desc": "Filter settings by name or description.",
"settings-noResults": "No settings match your search.",
"settings-proxyMode": "Proxy mode",
"settings-proxyMode-desc": "Choose how Legcord connects through a proxy. System uses your OS proxy (and HTTP(S)_PROXY environment variables). Fixed servers and PAC work like in a normal web browser. A restart is required.",
"settings-proxyMode-system": "System",
"settings-proxyMode-direct": "Direct (no proxy)",
"settings-proxyMode-fixed_servers": "Fixed servers",
"settings-proxyMode-pac_script": "PAC script",
"settings-proxyMode-auto_detect": "Auto-detect",
"settings-proxyRules": "Proxy server",
"settings-proxyRules-desc": "Proxy URL or Chromium proxy rules. Examples: http://127.0.0.1:8080, socks5://127.0.0.1:1080, or http=proxy:80;https=proxy:80",
"settings-proxyPacScript": "PAC script URL",
"settings-proxyPacScript-desc": "URL of a proxy auto-config (PAC) file, e.g. http://wpad/wpad.dat or file:///path/to/proxy.pac",
"settings-proxyBypassRules": "Proxy bypass list",
"settings-proxyBypassRules-desc": "Comma-separated hosts that skip the proxy. Use <local> for localhost. Example: <local>,*.intranet.example,10.0.0.0/8",
"menu-about": "About Legcord",
"menu-developerTools": "Developer tools",
"menu-openSettings": "Open settings",
@ -278,6 +243,8 @@
"settings-channel-stable": "Stable",
"settings-channel-canary": "Canary",
"settings-channel-ptb": "PTB",
"settings-category-powerManagement": "Power Management",
"settings-category-arrpc": "arRPC",
"settings-extendedPluginAbilities": "Extended plugin abilities",
"settings-extendedPluginAbilities-desc": "Allows plugins to read and write files in a scoped folder on your computer (e.g. for caching deleted messages). Only enable for plugins you trust—they can store data locally. Data is stored per plugin in Legcord's plugin-storage folder.",
"settings-audio-loopback": "Loopback",
@ -309,21 +276,12 @@
"detectable-appName": "App Name*",
"detectable-appId": "App ID*",
"detectable-themes": "Themes",
"detectable-themes-note": "Optional genre tags for this game, comma-separated.",
"detectable-aliases": "Aliases",
"detectable-aliases-note": "Optional alternate names Discord may match, comma-separated.",
"detectable-appId-note": "Discord application ID from the Developer Portal for this games rich presence.",
"detectable-enabled": "Enabled",
"detectable-placeholderName": "e.g. Discord",
"detectable-placeholderId": "e.g. 1234567890",
"detectable-placeholderThemes": "Action, Adventure",
"detectable-placeholderAliases": "Alias1, Alias2",
"themes-pageTitle": "Themes",
"themes-pageDesc": "Install BetterDiscord themes or write Quick CSS to customize Discords look. Themes are .theme.css files; Quick CSS is a single stylesheet you can edit anytime.",
"themes-empty": "No themes installed yet.",
"themes-emptyDesc": "Import a .theme.css file or paste a raw theme URL below. You can also browse community themes online.",
"themes-browseThemes": "Browse themes on BetterDiscord",
"themes-importUrlNote": "Paste a direct link to a raw .theme.css file (for example from GitHub).",
"themes-success": "Success!",
"themes-updated": "Theme successfully updated!",
"themes-bdInstalled": "BD theme successfully installed!",
@ -336,48 +294,23 @@
"themes-importFromFile": "Import from file",
"themes-openThemesFolder": "Open themes folder",
"themes-import": "Import",
"themes-refresh": "Refresh",
"themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]",
"themes-installed": "Installed Themes",
"keybinds-pageTitle": "Keybinds",
"keybinds-pageDesc": "Create keyboard shortcuts for Legcord actions like mute, deafen, or opening Quick CSS. Enable Global on a keybind to use it even when Discord is in the background.",
"keybinds-empty": "No keybinds added yet.",
"keybinds-emptyDesc": "Start by adding one with the button above.",
"games-registeredGames": "Registered Games",
"games-pageDesc": "Manage custom games for Rich Presence (arRPC). Pick a running process, then set its Discord application ID. Changes usually need a restart to apply.",
"games-refreshList": "Refresh list",
"games-add": "Add",
"games-remove": "Remove",
"games-removeConfirmHeader": "Remove game?",
"games-removeConfirmBody": "This game will be removed from rich presence. The client must be restarted for changes to take effect.",
"games-noExecutables": "No executables",
"games-empty": "No registered games yet.",
"games-emptyDesc": "Choose a process from the dropdown above, then click Add to register it for rich presence.",
"games-empty": "No registered games. Add one using the dropdown above.",
"games-lastDetected": "Last detected games",
"games-lastDetectedEmpty": "No games detected yet.",
"games-lastDetectedEmptyDesc": "Start a game with rich presence while Legcord is running to see it listed here.",
"games-lastDetectedEmpty": "No games detected yet. Start a game with rich presence to see them here.",
"games-blacklist": "Blacklist",
"games-blacklisted": "Blacklisted games",
"games-blacklistedEmpty": "No blacklisted games.",
"games-blacklistedEmptyDesc": "Games you blacklist wont show up in last detected.",
"games-removeFromBlacklist": "Remove",
"games-application": "Application",
"plugins-pageTitle": "Plugins",
"plugins-pageDesc": "Experimental filesystem plugins loaded from your Legcord plugins folder. Each plugin can run code in the main process, preload, and/or renderer. Only enable plugins you trust.",
"plugins-empty": "No runtime plugins found.",
"plugins-emptyDesc": "Open the plugins folder and add a plugin directory with a manifest.json, then refresh this list.",
"plugins-refresh": "Refresh list",
"plugins-openFolder": "Open plugins folder",
"plugins-reload": "Reload",
"plugins-targets": "Targets",
"plugins-targetsNone": "none",
"plugins-incompatible": "Plugin is not compatible.",
"plugins-toastTitle": "Plugins",
"plugins-toastEnableFailed": "Failed to enable {name}.",
"plugins-toastDisableFailed": "Failed to disable {name}.",
"plugins-toastReloaded": "Reloaded {name}.",
"plugins-toastReloadFailed": "Failed to reload {name}.",
"plugins-toastIncompatible": "{name} is not compatible with this Legcord version.",
"screenshare-selectSource": "Please select a source",
"screenshare-venmicDisabled": "Venmic disabled",
"screenshare-share": "Share",

View file

@ -95,12 +95,7 @@
"settings-prfmMode": "Modo rendimiento",
"settings-prfmMode-desc": "El modo de rendimiento es una función experimental de Legcord diseñada para optimizar la capacidad de respuesta y el rendimiento en función de tus necesidades. El impacto puede variar en función de tu hardware y uso, por lo que te recomendamos que pruebes cada modo para determinar cuál te conviene más.",
"settings-prfmMode-performance": "Rendimiento",
"settings-prfmMode-balanced": "Equilibrado",
"settings-prfmMode-battery": "Batería",
"settings-prfmMode-memory": "Ahorro de memoria",
"settings-prfmMode-voip": "Voz y vídeo",
"settings-prfmMode-latency": "Baja latencia",
"settings-prfmMode-smoothScreenshare": "Compartir pantalla fluido",
"settings-prfmMode-dynamic": "Dinámico",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Desactivar ganancia automática",

View file

@ -57,12 +57,7 @@
"settings-prfmMode": "Mode performance",
"settings-prfmMode-desc": "Le mode performance est une fonctionnalité expérimentale de Legcord conçue pour optimiser la réactivité et les performances selon vos besoins. Son impact peut varier selon votre matériel et votre utilisation, c'est pourquoi nous vous encourageons à essayer chaque mode pour déterminer lequel vous correspond le mieux.",
"settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Équilibré",
"settings-prfmMode-battery": "Batterie",
"settings-prfmMode-memory": "Économie mémoire",
"settings-prfmMode-voip": "Voix et vidéo",
"settings-prfmMode-latency": "Faible latence",
"settings-prfmMode-smoothScreenshare": "Partage d'écran fluide",
"settings-prfmMode-dynamic": "Dynamique",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableHttpCache": "Désactiver le cache HTTP",

View file

@ -94,12 +94,7 @@
"settings-prfmMode": "Teljesítmény mód",
"settings-prfmMode-desc": "A Teljesítmény mód egy kísérleti funkció a Legcordban, melynek célja a válaszidő és a teljesítmény optimalizálása az Ön igényei alapján. A hatás a hardvertől és a használattól függően változhat, ezért javasoljuk, hogy próbálja ki az egyes módokat, hogy megállapítsa, melyik működik legjobban az Ön számára.",
"settings-prfmMode-performance": "Teljesítmény",
"settings-prfmMode-balanced": "Kiegyensúlyozott",
"settings-prfmMode-battery": "Akkumulátor",
"settings-prfmMode-memory": "Memóriatakarékos",
"settings-prfmMode-voip": "Hang és videó",
"settings-prfmMode-latency": "Alacsony késleltetés",
"settings-prfmMode-smoothScreenshare": "Sima képernyőmegosztás",
"settings-prfmMode-dynamic": "Dinamikus",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Automatikus erősítés letiltása",

View file

@ -89,12 +89,7 @@
"settings-prfmMode": "Mode kinerja",
"settings-prfmMode-desc": "Mode Kinerja adalah fitur eksperimental di Legcord yang dirancang untuk mengoptimalkan responsivitas dan kinerja sesuai dengan kebutuhan Anda. Dampak yang dihasilkan dapat bervariasi tergantung pada spesifikasi perangkat keras dan pola penggunaan Anda, jadi kami menyarankan Anda untuk mencoba setiap mode untuk menentukan mana yang paling sesuai dengan kebutuhan Anda.",
"settings-prfmMode-performance": "Kinerja",
"settings-prfmMode-balanced": "Seimbang",
"settings-prfmMode-battery": "Baterai",
"settings-prfmMode-memory": "Penghemat memori",
"settings-prfmMode-voip": "Suara & video",
"settings-prfmMode-latency": "Latensi rendah",
"settings-prfmMode-smoothScreenshare": "Berbagi layar mulus",
"settings-prfmMode-dynamic": "Dinamis",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Nonaktifkan autogain",

View file

@ -145,12 +145,7 @@
"settings-prfmMode": "Modo de performance",
"settings-prfmMode-desc": "Modo de performance é uma funcionalidade experimental do Legcord feita para otimizar responsitividade e performance baseado nas suas nescessidades. Impacto pode variar dependendo da configuração da sua máquina e seu uso, então recomendamos tentar cada modo e escolher um que sirva melhor para você.",
"settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Equilibrado",
"settings-prfmMode-battery": "Bateria",
"settings-prfmMode-memory": "Economia de memória",
"settings-prfmMode-voip": "Voz e vídeo",
"settings-prfmMode-latency": "Baixa latência",
"settings-prfmMode-smoothScreenshare": "Compartilhamento de tela suave",
"settings-prfmMode-dynamic": "Dinâmico",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Desabilitar ganho automático",

View file

@ -45,12 +45,7 @@
"settings-audio-desc": "Selecione o método que o Legcord usa para capturar o áudio do seu sistema durante o partilhamento de ecrã.",
"settings-prfmMode": "Modo de desempenho",
"settings-prfmMode-performance": "Desempenho",
"settings-prfmMode-balanced": "Equilibrado",
"settings-prfmMode-dynamic": "Dinâmico",
"settings-prfmMode-latency": "Baixa latência",
"settings-prfmMode-memory": "Poupança de memória",
"settings-prfmMode-voip": "Voz e vídeo",
"settings-prfmMode-smoothScreenshare": "Partilha de ecrã fluida",
"settings-none": "Nenjum",
"settings-save": "Gravar configurações",
"settings-restart": "Reiniciar a app",

View file

@ -83,12 +83,7 @@
"settings-prfmMode": "Режим Производительности",
"settings-prfmMode-desc": "Режимы Производительности - экспериментальная функция Legcord, оптимизирующая производительность и скорость работы программы под ваши нужды. Эффект может зависеть от вашей системной конфигурации и паттернов использования, поэтому мы рекомендуем вам попробовать каждый режим и выбрать тот, что лучше всего подходит именно вам.",
"settings-prfmMode-performance": "Производительный",
"settings-prfmMode-balanced": "Сбалансированный",
"settings-prfmMode-battery": "Энергосбережение",
"settings-prfmMode-memory": "Экономия памяти",
"settings-prfmMode-voip": "Голос и видео",
"settings-prfmMode-latency": "Низкая задержка",
"settings-prfmMode-smoothScreenshare": "Плавная демонстрация экрана",
"settings-prfmMode-dynamic": "Динамический",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Выключить автоусиление",

View file

@ -89,12 +89,7 @@
"settings-prfmMode": "Режим продуктивності",
"settings-prfmMode-desc": "Режим продуктивності це експериментальна функція в Legcord, розроблена для оптимізації швидкості реагування та продуктивності відповідно до ваших потреб. Вплив може відрізнятися залежно від вашого обладнання та використання, тому ми рекомендуємо вам спробувати кожен режим, щоб визначити, який найкраще підходить саме вам.",
"settings-prfmMode-performance": "Продуктивність",
"settings-prfmMode-balanced": "Збалансований",
"settings-prfmMode-battery": "Батарея",
"settings-prfmMode-memory": "Економія пам'яті",
"settings-prfmMode-voip": "Голос і відео",
"settings-prfmMode-latency": "Низька затримка",
"settings-prfmMode-smoothScreenshare": "Плавна демонстрація екрана",
"settings-prfmMode-dynamic": "Динамічний",
"settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Вимкнути автоматичне посилення",

View file

@ -1,24 +1,6 @@
import type { Configuration } from "electron-builder";
import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
import { ACTION_FRIENDLY_NAMES, EXCLUDED_FROM_SHORTCUTS, ValidActions } from "./src/common/commandDefinitions";
const desktopActions = (exec: "AppRun" | "/opt/Legcord/legcord") =>
Object.fromEntries(
(Object.values(ValidActions) as ValidActions[])
.filter((action) => !EXCLUDED_FROM_SHORTCUTS.includes(action))
.map((action) => [
action,
{
Name: ACTION_FRIENDLY_NAMES[action],
Exec: `${exec} --${action} %U`,
},
]),
);
const availableActions = (Object.values(ValidActions) as ValidActions[])
.filter((action) => !EXCLUDED_FROM_SHORTCUTS.includes(action))
.join(";");
export const config: Configuration = {
appId: "app.legcord.Legcord",
@ -61,19 +43,6 @@ export const config: Configuration = {
},
},
appImage: {
desktop: {
entry: {
Actions: availableActions,
},
desktopActions: desktopActions("AppRun"),
},
},
pacman: {
depends: ["gtk3", "libnotify", "nss", "libxss", "libxtst", "xdg-utils", "at-spi2-core", "libsecret"],
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
@ -102,12 +71,6 @@ export const config: Configuration = {
category: "Network",
icon: "build/icon.icns",
depends: ["libgbm-dev", "libasound2", "libnspr4", "libnss3"],
desktop: {
entry: {
Actions: availableActions,
},
desktopActions: desktopActions("/opt/Legcord/legcord"),
},
},
files: [

View file

@ -43,7 +43,7 @@
"@xmldom/xmldom": "^0.9.10",
"app-builder-lib": "^26.8.1",
"babel-preset-solid": "^1.9.12",
"electron": "43.2.0",
"electron": "43.1.1",
"electron-builder": "26.8.1",
"lucide-solid": "^1.16.0",
"rolldown": "1.0.3",
@ -54,7 +54,6 @@
"xml-formatter": "^3.7.0"
},
"dependencies": {
"@jellybrick/dbus-next": "^0.11.1",
"adm-zip": "^0.6.0",
"arrpc": "github:Legcord/arrpc#efe7589762470d32b9ba10d529be5acc23cd0e19",
"electron-context-menu": "^4.1.2",

198
pnpm-lock.yaml generated
View file

@ -11,9 +11,6 @@ importers:
.:
dependencies:
'@jellybrick/dbus-next':
specifier: ^0.11.1
version: 0.11.1
adm-zip:
specifier: ^0.6.0
version: 0.6.0
@ -74,10 +71,10 @@ importers:
version: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1)
babel-preset-solid:
specifier: ^1.9.12
version: 1.9.12(@babel/core@7.29.7)(solid-js@1.9.14)
version: 1.9.12(@babel/core@8.0.0-rc.6)(solid-js@1.9.14)
electron:
specifier: 43.2.0
version: 43.2.0
specifier: 43.1.1
version: 43.1.1
electron-builder:
specifier: 26.8.1
version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
@ -116,10 +113,6 @@ packages:
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@8.0.0':
resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/code-frame@8.0.0-rc.6':
resolution: {integrity: sha512-Ru0EdYEptXbJGAGDMsenx+RcelHazuj8spqi7l0geXEPXv0X7qVisqWX+2pMaEZsWhS3Q6Um7oITwhnLe0cHJQ==}
engines: {node: ^22.18.0 || >=24.11.0}
@ -144,10 +137,6 @@ packages:
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/generator@8.0.0':
resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/generator@8.0.0-rc.6':
resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==}
engines: {node: ^22.18.0 || >=24.11.0}
@ -174,10 +163,6 @@ packages:
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@8.0.0':
resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/helper-globals@8.0.0-rc.6':
resolution: {integrity: sha512-ZF5FsxE4y7Eb6DiEbsLW3Gz3e73U/Uq3/ZotWf/moxv0DnZziXSMnW/tcXJF8b1mQE8XJnwPNSQOksKcKTz8kQ==}
engines: {node: ^22.18.0 || >=24.11.0}
@ -232,10 +217,6 @@ packages:
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@8.0.0':
resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/helper-string-parser@8.0.0-rc.6':
resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==}
engines: {node: ^22.18.0 || >=24.11.0}
@ -248,10 +229,6 @@ packages:
resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/helper-validator-identifier@8.0.4':
resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/helper-validator-option@7.29.7':
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
engines: {node: '>=6.9.0'}
@ -278,11 +255,6 @@ packages:
engines: {node: ^22.18.0 || >=24.11.0}
hasBin: true
'@babel/parser@8.0.4':
resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==}
engines: {node: ^22.18.0 || >=24.11.0}
hasBin: true
'@babel/plugin-syntax-jsx@7.29.7':
resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==}
engines: {node: '>=6.9.0'}
@ -317,10 +289,6 @@ packages:
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/template@8.0.0':
resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/template@8.0.0-rc.6':
resolution: {integrity: sha512-CZFjZblLiIUEFghbB3sKs3rpYrwo65rLIklw/gNpwBm326cU6TH/xzvJZlZ+H9HpDi00eDXqNJR8CtEKW3oc/A==}
engines: {node: ^22.18.0 || >=24.11.0}
@ -333,10 +301,6 @@ packages:
resolution: {integrity: sha512-/txuBViGaE9gpM8MSGShru1O2Bzp3zb3m/XKZNUsNv2SyNn++lKDDIWkiIQ/345j2FBy7CHiYTZuKhewHWyefw==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/traverse@8.0.4':
resolution: {integrity: sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
@ -345,10 +309,6 @@ packages:
resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==}
engines: {node: ^22.18.0 || >=24.11.0}
'@babel/types@8.0.4':
resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==}
engines: {node: ^22.18.0 || >=24.11.0}
'@biomejs/biome@2.4.16':
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
engines: {node: '>=14.21.3'}
@ -599,10 +559,6 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@jellybrick/dbus-next@0.11.1':
resolution: {integrity: sha512-MEN8boAd/XN3nTaRULNh/QDWPi1LzDXLLl83EV6AXsdWpZhBzagl8CjBC85g8769JMDhJFytVX8RKyVjUk9nAw==}
engines: {node: '>=12.20.0'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@ -654,9 +610,6 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@nodable/entities@3.0.0':
resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -1095,9 +1048,6 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
anynum@1.0.1:
resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==}
app-builder-bin@5.0.0-alpha.12:
resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==}
@ -1458,8 +1408,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'}
electron@43.2.0:
resolution: {integrity: sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==}
electron@43.1.1:
resolution: {integrity: sha512-I5c5vfuVvaXpWx3IZdwvXgxQW44+e7OP1wXGVQkogLeSFSkUZ6sLCcWV05AdEcs65AO5tAIJJwbp7ixw+LdarA==}
engines: {node: '>= 22.12.0'}
hasBin: true
@ -1556,13 +1506,6 @@ packages:
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-xml-builder@1.3.0:
resolution: {integrity: sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==}
fast-xml-parser@5.10.1:
resolution: {integrity: sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==}
hasBin: true
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@ -1828,9 +1771,6 @@ packages:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
is-unsafe@2.0.0:
resolution: {integrity: sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==}
isbinaryfile@4.0.10:
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
engines: {node: '>= 8.0.0'}
@ -2185,10 +2125,6 @@ packages:
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
path-expression-matcher@1.6.2:
resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==}
engines: {node: '>=14.0.0'}
path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@ -2510,9 +2446,6 @@ packages:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strnum@2.4.1:
resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==}
sumchecker@3.0.1:
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
engines: {node: '>= 8.0'}
@ -2677,10 +2610,6 @@ packages:
resolution: {integrity: sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==}
engines: {node: '>= 16'}
xml-naming@0.3.0:
resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
engines: {node: '>=16.0.0'}
xml-parser-xo@4.1.6:
resolution: {integrity: sha512-mXbSNSM+rIwndoxSwmFa83bOdeP8G/cOGr7XvxA67X7ebCzoAuVez9JYDCDub3zRDNBZxIbfjuXLSsamr7NfwQ==}
engines: {node: '>= 20'}
@ -2728,11 +2657,6 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/code-frame@8.0.0':
dependencies:
'@babel/helper-validator-identifier': 8.0.4
js-tokens: 10.0.0
'@babel/code-frame@8.0.0-rc.6':
dependencies:
'@babel/helper-validator-identifier': 8.0.0-rc.6
@ -2764,14 +2688,14 @@ snapshots:
'@babel/core@8.0.0-rc.6':
dependencies:
'@babel/code-frame': 8.0.0
'@babel/generator': 8.0.0
'@babel/code-frame': 8.0.0-rc.6
'@babel/generator': 8.0.0-rc.6
'@babel/helper-compilation-targets': 8.0.0-rc.6
'@babel/helpers': 8.0.0-rc.6
'@babel/parser': 8.0.4
'@babel/template': 8.0.0
'@babel/traverse': 8.0.4
'@babel/types': 8.0.4
'@babel/parser': 8.0.0-rc.6
'@babel/template': 8.0.0-rc.6
'@babel/traverse': 8.0.0-rc.6
'@babel/types': 8.0.0-rc.6
'@types/gensync': 1.0.5
convert-source-map: 2.0.0
find-up-simple: 1.0.1
@ -2789,19 +2713,10 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/generator@8.0.0':
dependencies:
'@babel/parser': 8.0.4
'@babel/types': 8.0.4
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@types/jsesc': 2.5.1
jsesc: 3.1.0
'@babel/generator@8.0.0-rc.6':
dependencies:
'@babel/parser': 8.0.0-rc.6
'@babel/types': 8.0.4
'@babel/types': 8.0.0-rc.6
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@types/jsesc': 2.5.1
@ -2842,8 +2757,6 @@ snapshots:
'@babel/helper-globals@7.29.7': {}
'@babel/helper-globals@8.0.0': {}
'@babel/helper-globals@8.0.0-rc.6': {}
'@babel/helper-member-expression-to-functions@7.29.7':
@ -2906,16 +2819,12 @@ snapshots:
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-string-parser@8.0.0': {}
'@babel/helper-string-parser@8.0.0-rc.6': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/helper-validator-identifier@8.0.0-rc.6': {}
'@babel/helper-validator-identifier@8.0.4': {}
'@babel/helper-validator-option@7.29.7': {}
'@babel/helper-validator-option@8.0.0-rc.6': {}
@ -2927,8 +2836,8 @@ snapshots:
'@babel/helpers@8.0.0-rc.6':
dependencies:
'@babel/template': 8.0.0
'@babel/types': 8.0.4
'@babel/template': 8.0.0-rc.6
'@babel/types': 8.0.0-rc.6
'@babel/parser@7.29.7':
dependencies:
@ -2936,17 +2845,18 @@ snapshots:
'@babel/parser@8.0.0-rc.6':
dependencies:
'@babel/types': 8.0.4
'@babel/parser@8.0.4':
dependencies:
'@babel/types': 8.0.4
'@babel/types': 8.0.0-rc.6
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@8.0.0-rc.6)':
dependencies:
'@babel/core': 8.0.0-rc.6
'@babel/helper-plugin-utils': 7.29.7
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
@ -2988,12 +2898,6 @@ snapshots:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/template@8.0.0':
dependencies:
'@babel/code-frame': 8.0.0
'@babel/parser': 8.0.4
'@babel/types': 8.0.4
'@babel/template@8.0.0-rc.6':
dependencies:
'@babel/code-frame': 8.0.0-rc.6
@ -3022,16 +2926,6 @@ snapshots:
'@babel/types': 8.0.0-rc.6
obug: 2.1.1
'@babel/traverse@8.0.4':
dependencies:
'@babel/code-frame': 8.0.0
'@babel/generator': 8.0.0
'@babel/helper-globals': 8.0.0
'@babel/parser': 8.0.4
'@babel/template': 8.0.0
'@babel/types': 8.0.4
obug: 2.1.1
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
@ -3042,11 +2936,6 @@ snapshots:
'@babel/helper-string-parser': 8.0.0-rc.6
'@babel/helper-validator-identifier': 8.0.0-rc.6
'@babel/types@8.0.4':
dependencies:
'@babel/helper-string-parser': 8.0.0
'@babel/helper-validator-identifier': 8.0.4
'@biomejs/biome@2.4.16':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.16
@ -3267,11 +3156,6 @@ snapshots:
dependencies:
minipass: 7.1.3
'@jellybrick/dbus-next@0.11.1':
dependencies:
fast-xml-builder: 1.3.0
fast-xml-parser: 5.10.1
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -3353,8 +3237,6 @@ snapshots:
'@tybys/wasm-util': 0.10.2
optional: true
'@nodable/entities@3.0.0': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -3717,8 +3599,6 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.2
anynum@1.0.1: {}
app-builder-bin@5.0.0-alpha.12: {}
app-builder-lib@26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1):
@ -3810,19 +3690,19 @@ snapshots:
- supports-color
optional: true
babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.7):
babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@8.0.0-rc.6):
dependencies:
'@babel/core': 7.29.7
'@babel/core': 8.0.0-rc.6
'@babel/helper-module-imports': 7.18.6
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@8.0.0-rc.6)
'@babel/types': 7.29.7
html-entities: 2.3.3
parse5: 7.3.0
babel-preset-solid@1.9.12(@babel/core@7.29.7)(solid-js@1.9.14):
babel-preset-solid@1.9.12(@babel/core@8.0.0-rc.6)(solid-js@1.9.14):
dependencies:
'@babel/core': 7.29.7
babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.7)
'@babel/core': 8.0.0-rc.6
babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@8.0.0-rc.6)
optionalDependencies:
solid-js: 1.9.14
@ -4208,7 +4088,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron@43.2.0:
electron@43.1.1:
dependencies:
'@electron-internal/extract-zip': 1.0.4
'@electron/get': 5.0.0
@ -4312,20 +4192,6 @@ snapshots:
fast-json-stable-stringify@2.1.0: {}
fast-xml-builder@1.3.0:
dependencies:
path-expression-matcher: 1.6.2
xml-naming: 0.3.0
fast-xml-parser@5.10.1:
dependencies:
'@nodable/entities': 3.0.0
fast-xml-builder: 1.3.0
is-unsafe: 2.0.0
path-expression-matcher: 1.6.2
strnum: 2.4.1
xml-naming: 0.3.0
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@ -4628,8 +4494,6 @@ snapshots:
is-plain-object@3.0.1: {}
is-unsafe@2.0.0: {}
isbinaryfile@4.0.10: {}
isbinaryfile@5.0.7: {}
@ -4925,8 +4789,6 @@ snapshots:
path-exists@5.0.0: {}
path-expression-matcher@1.6.2: {}
path-is-absolute@1.0.1: {}
path-key@3.1.1: {}
@ -5267,10 +5129,6 @@ snapshots:
strip-json-comments@2.0.1:
optional: true
strnum@2.4.1:
dependencies:
anynum: 1.0.1
sumchecker@3.0.1:
dependencies:
debug: 4.4.3
@ -5424,8 +5282,6 @@ snapshots:
dependencies:
xml-parser-xo: 4.1.6
xml-naming@0.3.0: {}
xml-parser-xo@4.1.6: {}
xmlbuilder@15.1.1: {}

View file

@ -6,7 +6,7 @@ allowBuilds:
koffi: true
minimumReleaseAgeExclude:
- electron@42.3.3 || 43.2.0
- electron@42.3.3
overrides:
yauzl: "^3.3.1" # Electron still can't get their shit together https://github.com/electron/electron/issues/51619

View file

@ -25,9 +25,6 @@ export interface AudioSettings {
loopbackType: "loopback" | "loopbackWithMute";
}
/** Chromium/Electron proxy modes — mirrors browser proxy settings. */
export type ProxyMode = "system" | "direct" | "fixed_servers" | "pac_script" | "auto_detect";
export interface Settings {
// Referenced for detecting a broken config.
"0"?: string;
@ -46,16 +43,7 @@ export interface Settings {
mods: ValidMods[];
mobileMode: boolean;
skipSplash: boolean;
performanceMode:
| "battery"
| "dynamic"
| "performance"
| "balanced"
| "memory"
| "voip"
| "latency"
| "smoothScreenshare"
| "none";
performanceMode: "battery" | "dynamic" | "performance" | "smoothScreenshare" | "none";
customJsBundle: RequestInfo | URL | string;
customCssBundle: RequestInfo | URL | string;
startMinimized: boolean;
@ -79,14 +67,6 @@ export interface Settings {
quickCss: boolean;
autoScroll: boolean;
additionalArguments: string;
/** How Legcord resolves HTTP(S) proxies (Chromium + main-process fetch). */
proxyMode: ProxyMode;
/** Fixed proxy rules, e.g. `http://127.0.0.1:8080` or `socks5://host:1080`. */
proxyRules: string;
/** Hosts that bypass the proxy (comma-separated), e.g. `<local>,*.intranet.example`. */
proxyBypassRules: string;
/** PAC script URL when proxyMode is `pac_script`. */
proxyPacScript: string;
noBundleUpdates: ValidMods[];
automaticUpdates: boolean;
overlayButtonColor: string;

View file

@ -1,28 +0,0 @@
// static command definitions to retrieve at build-time without calling electron-dev indirectly
export enum ValidActions {
mute = "mute",
deafen = "deafen",
leaveCall = "leave",
openSettings = "opensettings",
help = "help",
}
export const actionDescriptions: Record<ValidActions, string> = {
[ValidActions.mute]: "Toggle microphone mute",
[ValidActions.deafen]: "Toggle deafen (mute audio input/output)",
[ValidActions.leaveCall]: "Leave the current voice call",
[ValidActions.openSettings]: "Open the settings panel",
[ValidActions.help]: "Shows this help message",
};
export const ACTION_FRIENDLY_NAMES: Record<ValidActions, string> = {
[ValidActions.mute]: "Toggle Mute",
[ValidActions.deafen]: "Toggle Deafen",
[ValidActions.leaveCall]: "Leave Call",
[ValidActions.openSettings]: "Open Settings",
[ValidActions.help]: "ignore (help)",
};
// we don't need a 'show help' shortcut do we? be fr
export const EXCLUDED_FROM_SHORTCUTS: ValidActions[] = [ValidActions.help];

View file

@ -60,10 +60,6 @@ const defaults: Settings = {
noBundleUpdates: [],
automaticUpdates: false,
additionalArguments: "",
proxyMode: "system",
proxyRules: "",
proxyBypassRules: "<local>",
proxyPacScript: "",
customIcon: join(import.meta.dirname, "../", "/assets/desktop.png"),
smoothScroll: true,
autoScroll: false,

View file

@ -47,6 +47,5 @@ export function navigateTo(passedWindow: BrowserWindow, url: string): void {
passedWindow.webContents.executeJavaScript(
`history.pushState({}, null, ${JSON.stringify(sanitized)});window.dispatchEvent(new PopStateEvent("popstate", {}));`,
);
passedWindow.show();
passedWindow.focus();
}

View file

@ -32,76 +32,6 @@ const performance: Preset = {
disableFeatures: ["Vulkan"],
};
/** Light GPU boost without forcing the discrete GPU — a middle ground. */
const balanced: Preset = {
switches: [["enable-gpu-rasterization"], ["enable-zero-copy"], ["ignore-gpu-blocklist"]],
enableFeatures: [
"CanvasOopRasterization",
"UseSkiaRenderer",
"WebAssemblyLazyCompilation",
"CalculateNativeWinOcclusion",
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
],
disableFeatures: ["Vulkan"],
};
/** Reduce RAM / CPU usage at the cost of some visual smoothness. */
const memory: Preset = {
switches: [
["enable-low-end-device-mode"],
["enable-low-res-tiling"],
["process-per-site"],
["renderer-process-limit", "2"],
["force_low_power_gpu"],
["disk-cache-size", "67108864"], // 64 MB
["skia-resource-cache-limit-mb", "64"],
],
enableFeatures: ["CalculateNativeWinOcclusion", "TurnOffStreamingMediaCachingOnBattery"],
disableFeatures: [],
};
/** Favor voice/video call quality with HW WebRTC encode/decode. */
const voip: Preset = {
switches: [
["enable-gpu-rasterization"],
["enable-zero-copy"],
["ignore-gpu-blocklist"],
["force_high_performance_gpu"],
["disable-background-timer-throttling"],
["disable-renderer-backgrounding"],
["disable-backgrounding-occluded-windows"],
["enable-gpu-memory-buffer-video-frames"],
],
enableFeatures: [
"WebRtcHWDecoding",
"WebRtcHWEncoding",
"AcceleratedVideoDecoder",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecodeLinuxGL",
"AcceleratedVideoDecodeLinuxZeroCopyGL",
"ZeroCopyDesktopCapture",
],
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
};
/** Minimize input/render latency; keeps the app hot in the background. */
const latency: Preset = {
switches: [
["enable-gpu-rasterization"],
["enable-zero-copy"],
["ignore-gpu-blocklist"],
["force_high_performance_gpu"],
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
["disable-background-timer-throttling"],
["disable-renderer-backgrounding"],
["disable-backgrounding-occluded-windows"],
["disable-ipc-flooding-protection"],
["disable-backing-store-limit"],
],
enableFeatures: ["EnableDrDc", "CanvasOopRasterization", "UseSkiaRenderer", "WebAssemblyLazyCompilation"],
disableFeatures: ["Vulkan"],
};
const smoothExperiment: Preset = {
switches: [
["enable-gpu-rasterization"],
@ -124,7 +54,6 @@ const smoothExperiment: Preset = {
"AcceleratedVideoEncoder",
"AcceleratedVideoDecoder",
"AcceleratedVideoDecodeLinuxZeroCopyGL",
"ZeroCopyDesktopCapture",
],
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
};
@ -137,7 +66,7 @@ const battery: Preset = {
["enable-low-res-tiling"],
["process-per-site"],
],
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery", "CalculateNativeWinOcclusion"],
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery"],
disableFeatures: [],
};
@ -231,14 +160,6 @@ function mergeWithCustomFlags(preset: Preset): Preset {
};
}
function mergePresets(base: Preset, extra: Preset): Preset {
return {
switches: [...base.switches, ...extra.switches],
enableFeatures: [...base.enableFeatures, ...extra.enableFeatures],
disableFeatures: [...base.disableFeatures, ...extra.disableFeatures],
};
}
export function getPreset(): Preset | undefined {
// MIT License
@ -261,58 +182,31 @@ export function getPreset(): Preset | undefined {
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
let preset: Preset | undefined;
if (getConfig("vaapi")) {
console.log("VAAPI mode enabled");
mergeWithCustomFlags(vaapi);
}
switch (getConfig("performanceMode")) {
case "dynamic":
if (powerMonitor.isOnBatteryPower()) {
console.log("Battery mode enabled");
preset = battery;
return mergeWithCustomFlags(battery);
} else {
console.log("Performance mode enabled");
preset = performance;
return mergeWithCustomFlags(performance);
}
break;
case "performance":
console.log("Performance mode enabled");
preset = performance;
break;
case "balanced":
console.log("Balanced mode enabled");
preset = balanced;
break;
return mergeWithCustomFlags(performance);
case "battery":
console.log("Battery mode enabled");
preset = battery;
break;
case "memory":
console.log("Memory saver mode enabled");
preset = memory;
break;
case "voip":
console.log("Voice & video mode enabled");
preset = voip;
break;
case "latency":
console.log("Low latency mode enabled");
preset = latency;
break;
return mergeWithCustomFlags(battery);
case "smoothScreenshare":
console.log("Smooth screenshare mode enabled");
preset = smoothExperiment;
break;
return mergeWithCustomFlags(smoothExperiment);
default:
console.log("No performance modes set");
}
if (getConfig("vaapi")) {
console.log("VAAPI flags enabled");
preset = preset ? mergePresets(preset, vaapi) : vaapi;
}
if (preset) {
return mergeWithCustomFlags(preset);
}
}
/**

View file

@ -1,86 +0,0 @@
import { actionDescriptions, ValidActions } from "./commandDefinitions";
import { deafenToggle, leaveCall, muteToggle, openSettings } from "./keybindActions";
export function isValidAction(value: string): value is ValidActions {
return Object.values(ValidActions).some((action) => value.includes(action));
}
function findValidAction(str: string): ValidActions | undefined {
return Object.values(ValidActions).find((action) => str.includes(action));
}
function sanitizeArguments(args: string[]): string[] {
return args.filter((arg) => arg.startsWith("--")).map((arg) => arg.replace("--", ""));
}
/**
* Did the user pass any valid argument?
*
* @export
* @param {string[]} args List of arguments to validate
* @return {*} {boolean}
*/
export function passedValidArgument(args: string[]): boolean {
if (args.find((arg) => findValidAction(arg))) return true;
return false;
}
export function handleAction(action: ValidActions): void {
switch (action) {
case ValidActions.mute:
muteToggle();
break;
case ValidActions.deafen:
deafenToggle();
break;
case ValidActions.leaveCall:
leaveCall();
break;
case ValidActions.openSettings:
openSettings();
break;
case ValidActions.help:
showHelpMessage();
break;
default: {
// be completly sure we exaust every action possible at compile-time.
const exhaustiveCheck: never = action;
throw new Error(`Unhandled action: ${exhaustiveCheck}`);
}
}
}
/**
* Handles command line arguments and applies actions accordingly, without spawning a new Legcord instance.
*
* @export
* @param {string[]} args List of all arguments to treat as possible commands
*/
export function handleCommands(args: string[]): void {
const sanitazed_args = sanitizeArguments(args);
const handledActions = new Set<ValidActions>();
sanitazed_args.forEach((arg) => {
if (!arg) return;
if (!isValidAction(arg)) return;
const action = findValidAction(arg);
if (!action || handledActions.has(action)) return;
console.log(`valid action: ${arg}`);
handledActions.add(action);
handleAction(action);
});
}
function showHelpMessage(): void {
const entries = Object.entries(actionDescriptions);
const width = Math.max(...entries.map(([cmd]) => cmd.length));
console.log("\nAvailable commands (ignore '--' if over dbus):\n");
for (const [cmd, description] of entries) {
console.log(` --${cmd.padEnd(width)} ${description}`);
}
console.log("");
}

View file

@ -1,201 +0,0 @@
import { app, type ProxyConfig, session } from "electron";
import type { ProxyMode } from "../@types/settings.js";
import { getConfig } from "./config.js";
function envProxy(): string | undefined {
return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
}
function envNoProxy(): string | undefined {
return process.env.NO_PROXY || process.env.no_proxy;
}
/** Convert Chromium-style bypass list to NO_PROXY form. */
function toNoProxy(bypass: string): string {
return bypass
.split(/[,;]/)
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
if (part === "<local>") return "localhost,127.0.0.1,::1";
return part.replace(/^\*\./, ".");
})
.join(",");
}
/** Pick a single proxy URL Node fetch can use from Chromium proxyRules. */
function primaryProxyFromRules(rules: string): string | undefined {
const trimmed = rules.trim();
if (!trimmed) return undefined;
// Prefer an explicit https=/http=/socks= mapping, else first token.
const parts = trimmed
.split(";")
.map((p) => p.trim())
.filter(Boolean);
for (const scheme of ["https", "http", "socks", "socks5", "socks4"]) {
const match = parts.find((p) => p.toLowerCase().startsWith(`${scheme}=`));
if (match) {
const value = match
.slice(scheme.length + 1)
.split(",")[0]
?.trim();
if (value && value !== "direct://") return value.includes("://") ? value : `http://${value}`;
}
}
const first = parts[0]?.split(",")[0]?.trim();
if (!first || first === "direct://") return undefined;
if (first.includes("=")) {
const value = first.split("=")[1]?.trim();
if (!value || value === "direct://") return undefined;
return value.includes("://") ? value : `http://${value}`;
}
return first.includes("://") ? first : `http://${first}`;
}
function resolveProxyConfig(): ProxyConfig {
const mode = getConfig("proxyMode") as ProxyMode | undefined;
const proxyRules = (getConfig("proxyRules") ?? "").trim();
const proxyBypassRules = (getConfig("proxyBypassRules") ?? "").trim();
const proxyPacScript = (getConfig("proxyPacScript") ?? "").trim();
switch (mode) {
case "direct":
return { mode: "direct" };
case "auto_detect":
return {
mode: "auto_detect",
...(proxyBypassRules ? { proxyBypassRules } : {}),
};
case "pac_script":
return {
mode: "pac_script",
pacScript: proxyPacScript,
...(proxyBypassRules ? { proxyBypassRules } : {}),
};
case "fixed_servers":
return {
mode: "fixed_servers",
proxyRules,
...(proxyBypassRules ? { proxyBypassRules } : {}),
};
default: {
// system — also honor HTTP(S)_PROXY so env-based setups work for Chromium
const fromEnv = envProxy();
if (fromEnv) {
return {
mode: "fixed_servers",
proxyRules: fromEnv,
proxyBypassRules: proxyBypassRules || toNoProxy(envNoProxy() ?? "<local>"),
};
}
return {
mode: "system",
...(proxyBypassRules ? { proxyBypassRules } : {}),
};
}
}
}
/**
* Configure Node.js fetch / http to honor proxy env vars, and sync env from settings
* when using a fixed proxy. Must run before main-process network (e.g. mod downloads).
*/
export function configureNodeProxyEnv(): void {
process.env.NODE_USE_ENV_PROXY = "1";
const mode = (getConfig("proxyMode") as ProxyMode | undefined) ?? "system";
if (mode === "direct") {
// Force no proxy for Node fetches
process.env.HTTP_PROXY = "";
process.env.HTTPS_PROXY = "";
process.env.http_proxy = "";
process.env.https_proxy = "";
process.env.NO_PROXY = "*";
process.env.no_proxy = "*";
console.log("[Proxy] Node fetch: direct (no proxy)");
return;
}
if (mode === "fixed_servers") {
const rules = (getConfig("proxyRules") ?? "").trim();
const proxyUrl = primaryProxyFromRules(rules);
if (proxyUrl) {
process.env.HTTP_PROXY = proxyUrl;
process.env.HTTPS_PROXY = proxyUrl;
process.env.http_proxy = proxyUrl;
process.env.https_proxy = proxyUrl;
const bypass = (getConfig("proxyBypassRules") ?? "").trim();
if (bypass) {
const noProxy = toNoProxy(bypass);
process.env.NO_PROXY = noProxy;
process.env.no_proxy = noProxy;
}
console.log(`[Proxy] Node fetch: ${proxyUrl}`);
return;
}
}
// system / pac / auto_detect — keep existing env (HTTPS_PROXY etc.)
if (envProxy()) {
console.log(`[Proxy] Node fetch: using environment proxy (${envProxy()})`);
} else {
console.log("[Proxy] Node fetch: no HTTP(S)_PROXY in environment");
}
}
/**
* Apply Chromium command-line proxy switches early (before app ready).
* Session.setProxy still runs later for the authoritative config.
*/
export function applyProxyCommandLineSwitches(): void {
const mode = (getConfig("proxyMode") as ProxyMode | undefined) ?? "system";
const proxyRules = (getConfig("proxyRules") ?? "").trim();
const proxyBypassRules = (getConfig("proxyBypassRules") ?? "").trim();
const proxyPacScript = (getConfig("proxyPacScript") ?? "").trim();
if (mode === "direct") {
app.commandLine.appendSwitch("no-proxy-server");
return;
}
if (mode === "auto_detect") {
app.commandLine.appendSwitch("proxy-auto-detect");
if (proxyBypassRules) app.commandLine.appendSwitch("proxy-bypass-list", proxyBypassRules);
return;
}
if (mode === "pac_script" && proxyPacScript) {
app.commandLine.appendSwitch("proxy-pac-url", proxyPacScript);
if (proxyBypassRules) app.commandLine.appendSwitch("proxy-bypass-list", proxyBypassRules);
return;
}
if (mode === "fixed_servers" && proxyRules) {
app.commandLine.appendSwitch("proxy-server", proxyRules);
if (proxyBypassRules) app.commandLine.appendSwitch("proxy-bypass-list", proxyBypassRules);
return;
}
// system — if HTTPS_PROXY is set, push it to Chromium too
const fromEnv = envProxy();
if (fromEnv) {
app.commandLine.appendSwitch("proxy-server", fromEnv);
const bypass = proxyBypassRules || toNoProxy(envNoProxy() ?? "<local>");
app.commandLine.appendSwitch("proxy-bypass-list", bypass);
}
}
/** Apply proxy to the default session once Electron is ready. */
export async function applySessionProxy(): Promise<void> {
const config = resolveProxyConfig();
console.log(`[Proxy] Applying session proxy: ${JSON.stringify(config)}`);
try {
await session.defaultSession.setProxy(config);
await session.defaultSession.closeAllConnections();
} catch (error) {
console.error("[Proxy] Failed to apply session proxy:", error);
}
}

View file

@ -1,169 +0,0 @@
import type { Display, Rectangle } from "electron";
export const DEFAULT_WINDOW_WIDTH = 835;
export const DEFAULT_WINDOW_HEIGHT = 600;
export const MIN_WINDOW_WIDTH = 400;
export const MIN_WINDOW_HEIGHT = 300;
/** At least this much of the window must sit inside the workArea to count as visible. */
const MIN_VISIBLE_PX = 50;
export type WindowBoundsInput = {
width?: unknown;
height?: unknown;
x?: unknown;
y?: unknown;
displayId?: unknown;
};
export type SanitizedWindowBounds = {
x: number;
y: number;
width: number;
height: number;
displayId: number;
displayScaleFactor: number;
usedFallback: boolean;
};
function isFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
function intersectsWorkArea(rect: Rectangle, workArea: Rectangle, minVisible: number): boolean {
const left = Math.max(rect.x, workArea.x);
const top = Math.max(rect.y, workArea.y);
const right = Math.min(rect.x + rect.width, workArea.x + workArea.width);
const bottom = Math.min(rect.y + rect.height, workArea.y + workArea.height);
const visibleW = Math.max(0, right - left);
const visibleH = Math.max(0, bottom - top);
return visibleW >= minVisible && visibleH >= minVisible;
}
function fitSizeToWorkArea(width: number, height: number, workArea: Rectangle): { width: number; height: number } {
const fittedWidth = Math.min(Math.max(width, MIN_WINDOW_WIDTH), Math.max(workArea.width, MIN_WINDOW_WIDTH));
const fittedHeight = Math.min(Math.max(height, MIN_WINDOW_HEIGHT), Math.max(workArea.height, MIN_WINDOW_HEIGHT));
// Prefer staying within the workArea when it is at least the minimum size.
return {
width: workArea.width >= MIN_WINDOW_WIDTH ? Math.min(fittedWidth, workArea.width) : fittedWidth,
height: workArea.height >= MIN_WINDOW_HEIGHT ? Math.min(fittedHeight, workArea.height) : fittedHeight,
};
}
function fitRectToWorkArea(
width: number,
height: number,
x: number | undefined,
y: number | undefined,
workArea: Rectangle,
): Rectangle {
const { width: fittedWidth, height: fittedHeight } = fitSizeToWorkArea(width, height, workArea);
const maxX = workArea.x + Math.max(0, workArea.width - fittedWidth);
const maxY = workArea.y + Math.max(0, workArea.height - fittedHeight);
if (isFiniteNumber(x) && isFiniteNumber(y)) {
const candidate = { x, y, width: fittedWidth, height: fittedHeight };
if (intersectsWorkArea(candidate, workArea, MIN_VISIBLE_PX)) {
return {
x: clamp(x, workArea.x, maxX),
y: clamp(y, workArea.y, maxY),
width: fittedWidth,
height: fittedHeight,
};
}
}
// Missing / off-screen coords — center on this display.
return {
x: workArea.x + Math.round((workArea.width - fittedWidth) / 2),
y: workArea.y + Math.round((workArea.height - fittedHeight) / 2),
width: fittedWidth,
height: fittedHeight,
};
}
function findDisplayForPoint(displays: Display[], x: number, y: number): Display | undefined {
const containing = displays.find(
(d) =>
x >= d.bounds.x && y >= d.bounds.y && x < d.bounds.x + d.bounds.width && y < d.bounds.y + d.bounds.height,
);
if (containing) return containing;
let best: Display | undefined;
let bestDist = Number.POSITIVE_INFINITY;
for (const d of displays) {
const cx = clamp(x, d.bounds.x, d.bounds.x + d.bounds.width);
const cy = clamp(y, d.bounds.y, d.bounds.y + d.bounds.height);
const dx = x - cx;
const dy = y - cy;
const dist = dx * dx + dy * dy;
if (dist < bestDist) {
bestDist = dist;
best = d;
}
}
return best;
}
/**
* Sanitize saved/live window bounds so the window is always a usable size
* and visible on a connected display (preferring the remembered displayId).
*/
export function sanitizeWindowBounds(input: WindowBoundsInput, displays: Display[]): SanitizedWindowBounds {
const list = displays.length > 0 ? displays : [];
let usedFallback = false;
let width = isFiniteNumber(input.width) ? input.width : DEFAULT_WINDOW_WIDTH;
let height = isFiniteNumber(input.height) ? input.height : DEFAULT_WINDOW_HEIGHT;
if (!isFiniteNumber(input.width) || !isFiniteNumber(input.height)) {
usedFallback = true;
}
width = Math.max(MIN_WINDOW_WIDTH, width);
height = Math.max(MIN_WINDOW_HEIGHT, height);
const savedDisplayId = isFiniteNumber(input.displayId) ? input.displayId : undefined;
const x = isFiniteNumber(input.x) ? input.x : undefined;
const y = isFiniteNumber(input.y) ? input.y : undefined;
let target: Display | undefined;
if (savedDisplayId !== undefined) {
target = list.find((d) => d.id === savedDisplayId);
if (!target) usedFallback = true;
}
if (!target && x !== undefined && y !== undefined && list.length > 0) {
target = findDisplayForPoint(list, x, y);
}
if (!target) {
target = list[0];
usedFallback = true;
}
if (!target) {
return {
x: 0,
y: 0,
width,
height,
displayId: 0,
displayScaleFactor: 1,
usedFallback: true,
};
}
const fitted = fitRectToWorkArea(width, height, x, y, target.workArea);
return {
x: fitted.x,
y: fitted.y,
width: fitted.width,
height: fitted.height,
displayId: target.id,
displayScaleFactor: target.scaleFactor,
usedFallback,
};
}

View file

@ -1,158 +0,0 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { interface as dbusInterface, type ProxyInterface, sessionBus, Variant } from "@jellybrick/dbus-next";
import { ACTION_FRIENDLY_NAMES, EXCLUDED_FROM_SHORTCUTS, ValidActions } from "./common/commandDefinitions";
import { handleAction, isValidAction } from "./common/handleCommands";
const { Interface } = dbusInterface;
export const DBUS_INTERFACE_NAME = "app.legcord.Legcord";
export const DBUS_ADDRESS = "/app/legcord/Legcord";
const FREEDESKTOP_PORTAL_NAME = "org.freedesktop.portal.Desktop";
const FREEDESKTOP_PORTAL_ADDRESS = "/org/freedesktop/portal/desktop";
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html
type Shortcuts = [string, { description: Variant<string> }];
interface GlobalShortcuts extends ProxyInterface {
CreateSession(options: { handle_token: Variant; session_handle_token: Variant }): Promise<string>;
ConfigureShortcuts(session_handle: string, parent_window: string, options: object): Promise<string>;
BindShortcuts(
session_handle: string,
shortcuts: Shortcuts[],
parent_window: string,
options: {
handle_token: Variant;
},
): Promise<string>;
}
interface PropertiesInterface extends ProxyInterface {
Get(interface_name: string, property_name: string): Promise<Variant>;
Set(interface_name: string, property_name: string, value: Variant): void;
}
interface RegistryInterface extends ProxyInterface {
Register(app_id: string, options: object): void;
}
// original way of doing it (requires extensive babel plugins and typescript decorators)
// class LegcordInterface extends Interface {
// @method({ inSignature: "s", outSignature: "", noReply: true, disabled: false })
// TriggerAction(action: string): void {
// currentHandler(action);
// }
// }
// THIS is nescessary only so we don't have to use babel
// plugins all over rolldown.config.ts in every file, and slow down build time.
// https://acrisci.github.io/doc/node-dbus-next/
class LegcordInterface extends Interface {
constructor() {
super(DBUS_INTERFACE_NAME);
this.$methods = {
TriggerAction: {
name: "TriggerAction",
disabled: false,
noReply: true,
inSignature: "s",
outSignature: "",
inSignatureTree: [{ type: "s", child: [] }],
outSignatureTree: [],
fn: (action: string) => {
if (!isValidAction(action)) {
console.warn("Received unsupported action over DBus:", action);
return;
}
handleAction(action);
},
},
};
}
}
const bus = sessionBus();
var legcordInterface = new LegcordInterface();
function ensureDesktopFile(): void {
const dir = join(homedir(), ".local/share/applications");
const path = join(dir, `${DBUS_INTERFACE_NAME}.desktop`);
if (existsSync(path)) return;
mkdirSync(dir, { recursive: true });
writeFileSync(path, `[Desktop Entry]\nType=Application\nName=Legcord\nExec=${process.execPath}\nNoDisplay=true\n`);
}
function registerAppId(): Promise<void> {
ensureDesktopFile(); // we need a desktop file at XDG_PATH so we can associate with legcord's app_id
return bus.getProxyObject("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop").then((portalObj) => {
const registry: RegistryInterface = portalObj.getInterface("org.freedesktop.host.portal.Registry");
return registry.Register(DBUS_INTERFACE_NAME, {});
});
}
export async function setupGlobalShortcuts() {
const portalObj = await bus.getProxyObject(FREEDESKTOP_PORTAL_NAME, FREEDESKTOP_PORTAL_ADDRESS);
const properties: PropertiesInterface = portalObj.getInterface("org.freedesktop.DBus.Properties");
const globalShortcuts: GlobalShortcuts = portalObj.getInterface("org.freedesktop.portal.GlobalShortcuts");
const freedesktopVersion = (await properties.Get("org.freedesktop.portal.GlobalShortcuts", "version"))
.value as number;
console.debug(`Connected with freedesktop portal version ${freedesktopVersion}`);
await registerAppId();
console.debug(`Registered ${DBUS_INTERFACE_NAME} in freedesktop Registry.`);
function awaitResponse(requestPath: string): Promise<Record<string, unknown>> {
return bus.getProxyObject(FREEDESKTOP_PORTAL_NAME, requestPath).then(
(requestObj) =>
new Promise<Record<string, unknown>>((resolve, reject) => {
const requestIface = requestObj.getInterface("org.freedesktop.portal.Request");
requestIface.once("Response", (responseCode: number, results: Record<string, unknown>) => {
if (responseCode === 0) resolve(results);
else reject(new Error(`Request failed with code ${responseCode}`));
});
}),
);
}
const sessionRequestPath = await globalShortcuts.CreateSession({
handle_token: new Variant("s", "legcord_session"),
session_handle_token: new Variant("s", "legcord_shortcuts"),
});
const sessionResult = (await awaitResponse(sessionRequestPath)) as { session_handle: Variant<string> }; // real response signature
const actionList: [string, { description: Variant<string> }][] = (Object.values(ValidActions) as ValidActions[])
.filter((action) => !EXCLUDED_FROM_SHORTCUTS.includes(action))
.map((action) => [action, { description: new Variant("s", ACTION_FRIENDLY_NAMES[action]) }]);
const sessionHandle = sessionResult.session_handle.value;
const bindRequestPath = await globalShortcuts.BindShortcuts(sessionHandle, actionList, "", {
handle_token: new Variant("s", "legcord_bind"),
});
await awaitResponse(bindRequestPath);
globalShortcuts.on("Activated", (activatedSession: string, shortcutId: string) => {
if (activatedSession !== sessionHandle || !isValidAction(shortcutId)) return;
handleAction(shortcutId);
});
}
export async function startDbusService(): Promise<void> {
await bus.requestName(DBUS_INTERFACE_NAME);
bus.export(DBUS_ADDRESS, legcordInterface);
console.info(`Registered DBus service at ${DBUS_INTERFACE_NAME} ${DBUS_ADDRESS}`);
// console.debug(legcordInterface)
}
export function disconnectDbusService(): void {
bus.disconnect();
}

View file

@ -1,75 +0,0 @@
import { addScript, addStyle } from "../../common/dom.js";
const { ipcRenderer } = require("electron");
document.addEventListener("DOMContentLoaded", () => {
void (async () => {
const label =
((await ipcRenderer.invoke("getLang", "invite-goBackToApp")) as string | undefined) ||
"Go back to the Discord app";
addStyle("legcord://assets/css/inviteBack.css");
// Injected into the page world so Discord SPA history.pushState/replaceState is visible.
addScript(`
(() => {
const BTN_ID = "legcord-invite-back";
const LABEL = ${JSON.stringify(label)};
function isInvitePath(pathname) {
const path = (pathname || "").toLowerCase();
return path.startsWith("/invite/") || path.startsWith("/guest-invite/");
}
function ensureButton() {
let btn = document.getElementById(BTN_ID);
if (btn) return btn;
btn = document.createElement("button");
btn.id = BTN_ID;
btn.type = "button";
btn.textContent = LABEL;
btn.setAttribute("aria-label", LABEL);
btn.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
try {
history.pushState({}, "", "/app");
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }));
} catch (_) {
location.assign("/app");
}
});
(document.body || document.documentElement).appendChild(btn);
return btn;
}
function sync() {
const btn = ensureButton();
const onInvite = isInvitePath(location.pathname);
btn.setAttribute("data-visible", onInvite ? "true" : "false");
btn.hidden = !onInvite;
btn.setAttribute("aria-hidden", onInvite ? "false" : "true");
}
function patchHistory(method) {
const original = history[method];
if (typeof original !== "function") return;
history[method] = function () {
const result = original.apply(this, arguments);
queueMicrotask(sync);
return result;
};
}
patchHistory("pushState");
patchHistory("replaceState");
window.addEventListener("popstate", sync);
window.addEventListener("hashchange", sync);
sync();
// Fallback for Discord navigations that do not go through history hooks.
setInterval(sync, 1000);
})();
`);
})();
});

View file

@ -3,22 +3,15 @@ const { ipcRenderer } = require("electron");
import { addStyle } from "../../common/dom.js";
import { sleep } from "../../common/sleep.js";
const windowStyle = ipcRenderer.sendSync("getConfig", "windowStyle") as string;
const transparency = ipcRenderer.sendSync("getConfig", "transparency") as string;
const os = ipcRenderer.sendSync("getOS") as string;
// Native + transparency on macOS uses overlay chrome (see createWindow / Legcord#1095).
const usesOverlayChrome =
windowStyle === "default" ||
windowStyle === "overlay" ||
(windowStyle === "native" && os === "darwin" && transparency !== "none");
if (usesOverlayChrome) {
if (
ipcRenderer.sendSync("getConfig", "windowStyle") === "default" ||
ipcRenderer.sendSync("getConfig", "windowStyle") === "overlay"
) {
document.addEventListener("DOMContentLoaded", () => {
document.body.setAttribute("legcord-platform", os);
document.body.setAttribute("legcord-platform", ipcRenderer.sendSync("getOS"));
addStyle("legcord://assets/css/baseTitlebar.css");
sleep(500);
switch (os) {
switch (ipcRenderer.sendSync("getOS")) {
case "darwin":
// breaks traffic lights with bar__ and hidden__ classes
// document.body.setAttribute("class", "platform-osx");

View file

@ -9,7 +9,6 @@ import "./patches.mjs";
import "./newTitlebar.mjs";
import "./titlebar.mjs";
import "./themes.js";
import "./inviteBackButton.mjs";
console.log("Legcord");
window.localStorage.setItem("hideNag", "true");

View file

@ -1,49 +1,28 @@
import path from "node:path";
import { Worker } from "node:worker_threads";
import type { GameList, ServerSettings } from "arrpc";
import type { GameList } from "arrpc";
import type { BrowserWindow } from "electron";
import { getConfig } from "../common/config.js";
import { getDetectables } from "../common/detectables.js";
import { navigateTo } from "../common/dom.js";
/** Floor so a bad/zero config value cannot busy-loop the scanner. */
const MIN_SCAN_INTERVAL_MS = 1000;
const DEFAULT_SCAN_INTERVAL_MS = 5000;
let rpcWorker: Worker;
export let processList: GameList[] = [];
export function getRpcServerSettings(): ServerSettings {
const rawInterval = Number(getConfig("scanInterval"));
const scanInterval =
Number.isFinite(rawInterval) && rawInterval >= MIN_SCAN_INTERVAL_MS
? Math.floor(rawInterval)
: DEFAULT_SCAN_INTERVAL_MS;
return {
processScanning: Boolean(getConfig("processScanning")),
windowsLegacyScanning: Boolean(getConfig("windowsLegacyScanning")),
scanInterval,
};
}
export function startRPC(window: BrowserWindow) {
if (rpcWorker) {
rpcWorker.terminate();
}
const rpcPath = path.join(__dirname, "rpc.js");
const settings = getRpcServerSettings();
rpcWorker = new Worker(rpcPath, {
env: {
...process.env,
detectables: JSON.stringify(getDetectables()),
settings: JSON.stringify(settings),
},
});
rpcWorker.on("online", () => {
console.log("[arRPC] process started", settings);
console.log("[arRPC] process started");
console.log(rpcWorker.threadId);
});

View file

@ -17,18 +17,9 @@ import contextMenu from "electron-context-menu";
import { firstRun, getConfig, setConfig } from "../common/config.js";
import { navigateTo } from "../common/dom.js";
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
import { getLang } from "../common/lang.js";
import { injectThemesMain } from "../common/themes.js";
import {
DEFAULT_WINDOW_HEIGHT,
DEFAULT_WINDOW_WIDTH,
MIN_WINDOW_HEIGHT,
MIN_WINDOW_WIDTH,
sanitizeWindowBounds,
} from "../common/windowBounds.js";
import { getWindowState, setWindowState } from "../common/windowState.js";
import { disconnectDbusService } from "../dbus.js";
import { init } from "../main.js";
import { registerGlobalKeybinds } from "./globalKeybinds.js";
import { registerIpc } from "./ipc.js";
@ -41,17 +32,27 @@ import { registerVenmicIpc } from "./venmic.js";
export let mainWindows: BrowserWindow[] = [];
export let inviteWindow: BrowserWindow;
function getStoredWindowBounds() {
return sanitizeWindowBounds(
{
width: getWindowState("width"),
height: getWindowState("height"),
x: getWindowState("x"),
y: getWindowState("y"),
displayId: getWindowState("displayId"),
},
screen.getAllDisplays(),
);
function getStoredWindowBounds(): BrowserWindowConstructorOptions {
const width = getWindowState("width") ?? 835;
const height = getWindowState("height") ?? 600;
const x = getWindowState("x");
const y = getWindowState("y");
if (x === undefined || y === undefined) {
return {
width,
height,
};
}
// Return the stored window coordinates as-is.
// Restore uses setPosition/setSize for a direct API roundtrip.
return {
width,
height,
x,
y,
};
}
// Save window bounds using the same API family we restore with.
@ -60,25 +61,16 @@ function saveWindowState(win: BrowserWindow): void {
try {
const [x, y] = win.getPosition();
const [width, height] = win.getSize();
const sanitized = sanitizeWindowBounds(
{
width,
height,
x,
y,
displayId: screen.getDisplayNearestPoint({ x, y }).id,
},
screen.getAllDisplays(),
);
const display = screen.getDisplayNearestPoint({ x, y });
setWindowState({
width: sanitized.width,
height: sanitized.height,
width,
height,
isMaximized: win.isMaximized(),
x: sanitized.x,
y: sanitized.y,
displayId: sanitized.displayId,
displayScaleFactor: sanitized.displayScaleFactor,
x,
y,
displayId: display.id,
displayScaleFactor: display.scaleFactor,
});
} catch (e) {
console.log("[Window] Failed to save window state:", e);
@ -143,7 +135,7 @@ contextMenu({
function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
createTray();
if (getWindowState("isMaximized") ?? false) {
passedWindow.setSize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT); //just so the whole thing doesn't cover whole screen
passedWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen
passedWindow.maximize();
void passedWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
passedWindow.hide(); // please don't flashbang the user
@ -168,18 +160,17 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
app.on("second-instance", (_event, commandLine, _workingDirectory, additionalData) => {
void (async () => {
// Print out data received from the second instance.
console.log(`data received: ${additionalData}`);
console.log(additionalData);
if (!getConfig("multiInstance")) {
// Someone tried to run a second instance,
// we should focus our window if the user is not running special commands.
if (passedWindow && !passedValidArgument(commandLine)) {
// Someone tried to run a second instance, we should focus our window.
if (passedWindow) {
if (passedWindow.isMinimized()) passedWindow.restore();
passedWindow.show();
passedWindow.focus();
}
if (commandLine && commandLine.length > 0) {
handleCommands(commandLine);
console.log(commandLine);
const lastArg = commandLine.pop();
if (lastArg?.startsWith("discord://-")) {
navigateTo(passedWindow, lastArg.replace("discord://-", ""));
@ -392,7 +383,6 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
});
app.on("before-quit", () => {
stopRPC();
disconnectDbusService();
try {
// Ensure current window state is saved with display info
if (passedWindow && !passedWindow.isDestroyed()) saveWindowState(passedWindow);
@ -494,15 +484,10 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
export function createWindow() {
const storedBounds = getStoredWindowBounds();
if (storedBounds.usedFallback) {
console.log("[Window] Stored bounds were invalid or off-screen; using sanitized placement", storedBounds);
}
const browserWindowOptions: BrowserWindowConstructorOptions = {
// Use safe defaults for constructor; actual bounds applied via setPosition/setSize below
width: DEFAULT_WINDOW_WIDTH,
height: DEFAULT_WINDOW_HEIGHT,
minWidth: MIN_WINDOW_WIDTH,
minHeight: MIN_WINDOW_HEIGHT,
// Use safe defaults for constructor; actual bounds applied via setBounds() below
width: 835,
height: 600,
title: "Legcord",
show: false,
darkTheme: true,
@ -527,22 +512,7 @@ export function createWindow() {
}
break;
case "native":
// On macOS, frame:true + transparent/vibrancy makes the native title bar
// and traffic lights invisible (Legcord#1095). Use overlay chrome instead.
if (os.platform() === "darwin" && getConfig("transparency") !== "none") {
browserWindowOptions.titleBarStyle = "hidden";
browserWindowOptions.titleBarOverlay = {
color: getConfig("overlayButtonColor"),
symbolColor: "#99aab5",
height: 30,
};
browserWindowOptions.trafficLightPosition = {
x: 10,
y: 10,
};
} else {
browserWindowOptions.frame = true;
}
browserWindowOptions.frame = true;
break;
case "overlay":
browserWindowOptions.titleBarStyle = "hidden";
@ -580,8 +550,15 @@ export function createWindow() {
const mainWindow = new BrowserWindow(browserWindowOptions);
// Restore by position + size directly to match saveWindowState roundtrip.
mainWindow.setPosition(storedBounds.x, storedBounds.y);
mainWindow.setSize(storedBounds.width, storedBounds.height);
if (
storedBounds.x !== undefined &&
storedBounds.y !== undefined &&
storedBounds.width !== undefined &&
storedBounds.height !== undefined
) {
mainWindow.setPosition(storedBounds.x, storedBounds.y);
mainWindow.setSize(storedBounds.width, storedBounds.height);
}
mainWindows.push(mainWindow);
doAfterDefiningTheWindow(mainWindow);

View file

@ -18,8 +18,6 @@ import {
} from "./common/config.js";
import { getPreset } from "./common/flags.js";
import { setLang } from "./common/lang.js";
import { applyProxyCommandLineSwitches, applySessionProxy, configureNodeProxyEnv } from "./common/proxy.js";
import { setupGlobalShortcuts, startDbusService } from "./dbus.js";
// Chrome flags tracking
export interface AppliedFlagsOutput {
@ -155,7 +153,7 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
.add("MediaSessionService");
// Your data now belongs to CCP
crashReporter.start({ uploadToServer: false });
// enable pulseaudio audio sharing on linux and register keybinds on supported desktop managers and start dbus socket
// enable pulseaudio audio sharing on linux
if (process.platform === "linux") {
app.commandLine.appendSwitch("gtk-version", "3");
trackSwitch("gtk-version", "3");
@ -163,16 +161,6 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
disableFeatures.add("WebRtcAllowInputVolumeAdjustment");
app.commandLine.appendSwitch("enable-speech-dispatcher");
trackSwitch("enable-speech-dispatcher");
startDbusService()
.catch((reason) => {
console.error("Could not start DBus service.", reason);
})
.then(() => {
setupGlobalShortcuts().catch((reason) => {
console.error("Could not setup global shortcuts.", reason);
});
});
}
// enable webrtc capturer for wayland
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") {
@ -194,8 +182,6 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
app.commandLine.appendSwitch("enable-transparent-visuals");
trackSwitch("enable-transparent-visuals");
checkIfConfigIsBroken();
configureNodeProxyEnv();
applyProxyCommandLineSwitches();
const preset = getPreset();
if (preset) {
preset.switches.forEach(([key, val]) => {
@ -336,7 +322,6 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
void app.whenReady().then(async () => {
if (isDev) console.log(JSON.stringify(getAppliedFlags()));
await applySessionProxy();
process.on("SIGINT", () => app.quit());
process.on("SIGTERM", () => app.quit());
// Patch for linux bug to ensure things are loaded before window creation (fixes transparency on some linux systems)

View file

@ -5,16 +5,9 @@ import { parentPort } from "node:worker_threads";
import RPCServer, { type GameList, type ServerSettings } from "arrpc";
const detectables: GameList = process.env.detectables ? JSON.parse(process.env.detectables) : [];
const defaultSettings: ServerSettings = {
processScanning: true,
windowsLegacyScanning: false,
scanInterval: 5000,
};
const settings: ServerSettings = process.env.settings
? { ...defaultSettings, ...JSON.parse(process.env.settings) }
: defaultSettings;
console.log("[arRPC] worker settings", settings);
? JSON.parse(process.env.settings)
: { processScanning: true, windowsLegacyScanning: false, scanInterval: 5000 };
const RPC = await new RPCServer(detectables, settings);
// Guard parentPort

View file

@ -1,6 +0,0 @@
.fieldNote {
display: block;
margin: 4px 0 8px;
color: var(--text-muted, var(--text-subtle));
line-height: 1.35;
}

View file

@ -1,7 +1,6 @@
import type { Game } from "arrpc";
import { createSignal } from "solid-js";
import { setRestartRequired } from "../settings.js";
import classes from "./AddDetectableModal.module.css";
const {
ui: {
@ -17,8 +16,6 @@ const {
SwitchItem,
showToast,
openConfirmationModal,
Text,
TextTags,
},
plugin: { store },
} = shelter;
@ -91,25 +88,16 @@ export const AddDetectableModal = (props: { close: () => void; executable: strin
<ModalRoot size={ModalSizes.SMALL}>
<ModalHeader close={props.close}>{t["detectable-addApp"]}</ModalHeader>
<ModalBody>
<Header tag={HeaderTags.HeadingSM}>{t["detectable-appName"]}</Header>
<Header tag={HeaderTags.H5}>{t["detectable-appName"]}</Header>
<TextBox value={appName()} onInput={setAppName} placeholder={t["detectable-placeholderName"]} />
<Divider mt mb />
<Header tag={HeaderTags.HeadingSM}>{t["detectable-appId"]}</Header>
<Text tag={TextTags.textSM} class={classes.fieldNote}>
{t["detectable-appId-note"]}
</Text>
<Header tag={HeaderTags.H5}>{t["detectable-appId"]}</Header>
<TextBox value={appId()} onInput={setAppId} placeholder={t["detectable-placeholderId"]} />
<Divider mt mb />
<Header tag={HeaderTags.HeadingSM}>{t["detectable-themes"]}</Header>
<Text tag={TextTags.textSM} class={classes.fieldNote}>
{t["detectable-themes-note"]}
</Text>
<Header tag={HeaderTags.H5}>{t["detectable-themes"]}</Header>
<TextBox value={themes()} onInput={setThemes} placeholder={t["detectable-placeholderThemes"]} />
<Divider mt mb />
<Header tag={HeaderTags.HeadingSM}>{t["detectable-aliases"]}</Header>
<Text tag={TextTags.textSM} class={classes.fieldNote}>
{t["detectable-aliases-note"]}
</Text>
<Header tag={HeaderTags.H5}>{t["detectable-aliases"]}</Header>
<TextBox value={aliases()} onInput={setAliases} placeholder={t["detectable-placeholderAliases"]} />
<Divider mt mb />
<SwitchItem hideBorder value={enabled()} onChange={setEnabled}>

View file

@ -45,12 +45,3 @@
.item {
margin-bottom: 20px;
}
.embedded {
margin-bottom: 0;
}
.embedded .actions {
margin-top: 0;
margin-bottom: 0;
}

View file

@ -1,4 +1,4 @@
import { createSignal, Show } from "solid-js";
import { createSignal } from "solid-js";
import classes from "./BackupSection.module.css";
const {
@ -143,7 +143,7 @@ function applyClientModsFromRestore(raw: unknown): void {
}
}
export function BackupSection(props: { embedded?: boolean }) {
export function BackupSection() {
const t = store.i18n;
function buildClientMods(includes: IncludeOptions) {
@ -248,13 +248,11 @@ export function BackupSection(props: { embedded?: boolean }) {
}
return (
<div class={props.embedded ? classes.embedded : classes.item}>
<Show when={!props.embedded}>
<Header class={classes.title} tag={HeaderTags.H3}>
{t["backup-pageTitle"]}
</Header>
<div class={classes.note}>{t["backup-pageSubtitle"]}</div>
</Show>
<div class={classes.item}>
<Header class={classes.title} tag={HeaderTags.H3}>
{t["backup-pageTitle"]}
</Header>
<div class={classes.note}>{t["backup-pageSubtitle"]}</div>
<div class={classes.actions}>
<Button onClick={openBackupModal} size={ButtonSizes.LARGE}>
{t["backup-createBackup"]}
@ -263,9 +261,7 @@ export function BackupSection(props: { embedded?: boolean }) {
{t["backup-restore"]}
</Button>
</div>
<Show when={!props.embedded}>
<Divider />
</Show>
<Divider />
</div>
);
}

View file

@ -1,27 +0,0 @@
.empty {
margin: 1rem 0 1.5rem;
padding: 1rem;
border: 1px solid var(--border-subtle, var(--background-modifier-accent));
border-radius: 8px;
background: var(--background-secondary, transparent);
}
.message {
margin: 0;
color: var(--header-primary, var(--text-normal));
}
.description {
margin-top: 6px;
color: var(--text-muted, var(--text-subtle));
line-height: 1.35;
max-width: 36rem;
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 12px;
}

View file

@ -1,38 +0,0 @@
import { Show } from "solid-js";
import classes from "./EmptyState.module.css";
const {
ui: { Header, HeaderTags, Text, TextTags, Button, ButtonSizes, LinkButton },
} = shelter;
export function EmptyState(props: {
message: string;
description?: string;
actionLabel?: string;
onAction?: () => void;
linkHref?: string;
linkLabel?: string;
}) {
return (
<div class={classes.empty}>
<Header tag={HeaderTags.HeadingSM} class={classes.message}>
{props.message}
</Header>
<Show when={props.description}>
<Text tag={TextTags.textSM} class={classes.description}>
{props.description}
</Text>
</Show>
<div class={classes.actions}>
<Show when={props.actionLabel && props.onAction}>
<Button size={ButtonSizes.LARGE} onClick={props.onAction}>
{props.actionLabel}
</Button>
</Show>
<Show when={props.linkHref && props.linkLabel}>
<LinkButton href={props.linkHref!}>{props.linkLabel}</LinkButton>
</Show>
</div>
</div>
);
}

View file

@ -3,7 +3,6 @@ import classes from "./PluginCard.module.css";
const {
ui: { Header, HeaderTags, Switch, Button, ButtonSizes },
plugin: { store },
} = shelter;
export function PluginCard(props: {
@ -12,7 +11,6 @@ export function PluginCard(props: {
onToggle: (enabled: boolean) => void;
onReload: () => void;
}) {
const t = store.i18n;
const targets = [
props.plugin.hasMain ? "main" : null,
props.plugin.hasPreload ? "preload" : null,
@ -25,7 +23,7 @@ export function PluginCard(props: {
<div class={classes.card}>
<div class={classes.main}>
<div class={classes.titleRow}>
<Header tag={HeaderTags.HeadingLG} class={classes.title}>
<Header tag={HeaderTags.H3} class={classes.title}>
{props.plugin.name}
</Header>
<Switch checked={props.plugin.enabled} onChange={props.onToggle} disabled={props.busy} />
@ -35,12 +33,10 @@ export function PluginCard(props: {
{props.plugin.author ? `${props.plugin.author}` : ""}
</div>
{props.plugin.description ? <div class={classes.description}>{props.plugin.description}</div> : null}
<div class={classes.targets}>
{t["plugins-targets"]}: {targets || t["plugins-targetsNone"]}
</div>
<div class={classes.targets}>Targets: {targets || "none"}</div>
{!props.plugin.compatible ? (
<div class={classes.description}>
{props.plugin.compatibilityMessage ?? t["plugins-incompatible"]}
{props.plugin.compatibilityMessage ?? "Plugin is not compatible."}
</div>
) : null}
</div>
@ -49,9 +45,8 @@ export function PluginCard(props: {
size={ButtonSizes.SMALL}
onClick={props.onReload}
disabled={props.busy || !props.plugin.enabled}
tooltip={t["plugins-reload"]}
>
{t["plugins-reload"]}
Reload
</Button>
</div>
</div>

View file

@ -1,10 +0,0 @@
.header {
margin-bottom: 4px;
}
.description {
margin-top: 4px;
color: var(--text-muted, var(--text-subtle));
max-width: 42rem;
line-height: 1.35;
}

View file

@ -1,28 +0,0 @@
import type { JSX } from "solid-js";
import { Show } from "solid-js";
import classes from "./SettingsPageHeader.module.css";
const {
ui: { Header, HeaderTags, Text, TextTags, Divider },
} = shelter;
export function SettingsPageHeader(props: {
title: string;
description: string;
children?: JSX.Element;
/** Render a Divider under the header block (default true). */
divider?: boolean;
}) {
return (
<div class={classes.header}>
<Header tag={HeaderTags.HeadingXXL}>{props.title}</Header>
<Text tag={TextTags.textSM} class={classes.description}>
{props.description}
</Text>
<Show when={props.children}>{props.children}</Show>
<Show when={props.divider !== false}>
<Divider mt mb />
</Show>
</div>
);
}

View file

@ -1,111 +0,0 @@
.container {
display: flex;
flex-direction: column;
margin-top: 16px;
border: 1px solid var(--border-subtle, var(--background-modifier-accent));
border-radius: 8px;
background-color: var(--background-surface-higher, var(--background-secondary));
text-align: left;
}
.containerOpened {
margin-bottom: 16px;
}
.header {
display: flex;
align-items: center;
width: 100%;
padding: 16px;
gap: 16px;
cursor: pointer;
border: none;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
}
.header:focus-visible {
outline: 2px solid var(--focus-primary, var(--brand-500));
outline-offset: -2px;
border-radius: 8px;
}
.icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 12px;
background-color: var(--background-base-lowest, var(--background-tertiary));
color: var(--interactive-text-default, var(--interactive-normal));
}
.title {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.titleText {
cursor: pointer;
margin: 0;
}
.description {
color: var(--interactive-text-default, var(--text-muted));
font-size: 12px;
line-height: 16px;
font-weight: 500;
}
.caret {
margin-left: auto;
color: var(--interactive-text-default, var(--interactive-normal));
display: flex;
align-items: center;
}
.caretIcon {
transition: none;
}
.section {
display: flex;
flex-direction: column;
max-height: 0;
padding-top: 0;
overflow: hidden;
gap: 8px;
border-width: 0;
}
.sectionOpened {
max-height: none;
/* Allow dropdown menus to paint outside the panel instead of being clipped. */
overflow: visible;
margin: 16px;
margin-top: 0;
padding-top: 16px;
border-top: 1px solid var(--border-subtle, var(--background-modifier-accent));
}
:global(.full-motion) .container {
transition: margin-bottom 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
:global(.full-motion) .caretIcon {
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Avoid transitioning max-height: none ↔ 0 — it visually collapses on content reflow. */
:global(.full-motion) .section {
transition:
padding-top 0.5s cubic-bezier(0.16, 1, 0.3, 1),
margin-top 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

View file

@ -1,83 +0,0 @@
import type { Component, JSX } from "solid-js";
import { createSignal, Show } from "solid-js";
import classes from "./SettingsPanel.module.css";
const {
ui: { Header, HeaderTags },
} = shelter;
/** Survives SettingsPanel / SettingsPage remounts (e.g. after toggling a setting). */
const persistedOpen: Record<string, boolean> = {};
export type SettingsPanelProps = {
/** Stable id used to remember open/closed across remounts. */
id: string;
title: string;
description?: string;
icon: Component;
children: JSX.Element;
/**
* When true, the panel is forced open (search). Manual toggles are ignored
* until forceOpen is cleared matching Shelters settings panels.
*/
forceOpen?: boolean;
defaultOpen?: boolean;
/** Hide the panel entirely (e.g. no search matches). */
hidden?: boolean;
};
const CaretIcon = (props: { open: boolean }) => (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="none"
viewBox="0 0 24 24"
class={classes.caretIcon}
style={{ transform: props.open ? "rotate(180deg)" : "rotate(0deg)" }}
>
<path
fill="currentColor"
d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z"
/>
</svg>
);
export function SettingsPanel(props: SettingsPanelProps) {
const [internalOpen, setInternalOpen] = createSignal(persistedOpen[props.id] ?? props.defaultOpen ?? false);
const isOpen = () => Boolean(props.forceOpen) || internalOpen();
function toggle() {
if (props.forceOpen) return;
const next = !internalOpen();
persistedOpen[props.id] = next;
setInternalOpen(next);
}
return (
<div
class={`${classes.container} ${isOpen() ? classes.containerOpened : ""}`}
style={{ display: props.hidden ? "none" : undefined }}
>
<button type="button" class={classes.header} onClick={toggle} aria-expanded={isOpen()}>
<div class={classes.icon}>
<props.icon />
</div>
<div class={classes.title}>
<Header tag={HeaderTags.H5} class={classes.titleText}>
{props.title}
</Header>
<Show when={props.description}>
<div class={classes.description}>{props.description}</div>
</Show>
</div>
<div class={classes.caret}>
<CaretIcon open={isOpen()} />
</div>
</button>
<div class={`${classes.section} ${isOpen() ? classes.sectionOpened : ""}`}>{props.children}</div>
</div>
);
}

View file

@ -1,26 +0,0 @@
import type { Accessor, JSX, ParentProps } from "solid-js";
import { createContext, Show, useContext } from "solid-js";
const SearchQueryContext = createContext<Accessor<string>>(() => "");
export function SettingsSearchProvider(props: ParentProps<{ query: Accessor<string> }>) {
return <SearchQueryContext.Provider value={props.query}>{props.children}</SearchQueryContext.Provider>;
}
export function useSettingsSearchQuery(): Accessor<string> {
return useContext(SearchQueryContext);
}
export function matchesSettingsQuery(keywords: Array<string | undefined | null>, query: string): boolean {
const q = query.trim().toLowerCase();
if (!q) return true;
return keywords.some((keyword) => {
if (!keyword) return false;
return keyword.toLowerCase().includes(q);
});
}
export function SearchableSetting(props: { keywords: Array<string | undefined | null>; children: JSX.Element }) {
const query = useSettingsSearchQuery();
return <Show when={matchesSettingsQuery(props.keywords, query())}>{props.children}</Show>;
}

View file

@ -52,7 +52,7 @@ export const ThemesCard = (props: { theme: ThemeManifest }) => {
<div class={classes.card}>
<div class={classes.topRow}>
<div class={classes.info}>
<Header tag={HeaderTags.HeadingLG} class={classes.title}>
<Header tag={HeaderTags.H2} class={classes.title}>
{props.theme.name}
</Header>
<div class={classes.author}>
@ -66,23 +66,18 @@ export const ThemesCard = (props: { theme: ThemeManifest }) => {
</Show>
<div class={classes.actions}>
<button
use:tooltip={store.i18n["themes-delete"]}
title={store.i18n["themes-delete"]}
type="button"
onClick={removeTheme}
class={`${classes.btn} ${classes.btnDanger}`}
>
<img class={classes.icon} alt={store.i18n["themes-delete"]} src="legcord://assets/Trash.png" />
</button>
<button use:tooltip={store.i18n["themes-edit"]} type="button" onClick={editTheme} class={classes.btn}>
<button title={store.i18n["themes-edit"]} type="button" onClick={editTheme} class={classes.btn}>
<img class={classes.icon} alt={store.i18n["themes-edit"]} src="legcord://assets/Edit.png" />
</button>
<Show when={showUpdate}>
<button
use:tooltip={store.i18n["themes-update"]}
type="button"
onClick={updateTheme}
class={classes.btn}
>
<button title={store.i18n["themes-update"]} type="button" onClick={updateTheme} class={classes.btn}>
<img
class={classes.icon}
alt={store.i18n["themes-update"]}
@ -90,12 +85,7 @@ export const ThemesCard = (props: { theme: ThemeManifest }) => {
/>
</button>
</Show>
<button
use:tooltip={store.i18n["themes-open"]}
type="button"
onClick={openThemesFolder}
class={classes.btn}
>
<button title={store.i18n["themes-open"]} type="button" onClick={openThemesFolder} class={classes.btn}>
<img class={classes.icon} alt={store.i18n["themes-open"]} src="legcord://assets/Folder.png" />
</button>
</div>

View file

@ -1,111 +0,0 @@
/**
* Category icons for settings panels.
* Filled 24×24 paths match Discords design-system icons from
* https://docs.discord.com/developers/intro (Bots, Activities, Social Layer, etc.).
*/
import type { JSX } from "solid-js";
const ICON_SIZE = 20;
const PanelIcon = (props: { children: JSX.Element; viewBox?: string }) => (
<svg
width={ICON_SIZE}
height={ICON_SIZE}
viewBox={props.viewBox ?? "0 0 24 24"}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{props.children}
</svg>
);
/** Discord docs — download / archive style (Backup). */
export const BackupPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
d="M12 2a1 1 0 0 1 1 1v10.17l2.18-2.17a1 1 0 1 1 1.42 1.42l-3.88 3.88a1.13 1.13 0 0 1-1.6 0L7.24 12.42a1 1 0 1 1 1.42-1.42L10.83 13.17V3a1 1 0 0 1 1-1ZM5 18a1 1 0 0 0 1 1h12a1 1 0 1 0 0-2H6a1 1 0 0 0-1 1Z"
/>
</PanelIcon>
);
/** Discord docs — Bots & Apps robot. */
export const ModsPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
d="M7.89 13.46a1 1 0 0 1-1.78-.9L7 13l-.9-.45.01-.01.01-.02a2.42 2.42 0 0 1 .14-.23c.1-.14.23-.31.4-.5.37-.36.98-.79 1.84-.79.86 0 1.47.43 1.83.8a3.28 3.28 0 0 1 .55.72v.02h.01v.01L10 13l.9-.45a1 1 0 0 1-1.79.9 1.28 1.28 0 0 0-.19-.25c-.14-.13-.28-.2-.42-.2-.14 0-.28.07-.42.2a1.28 1.28 0 0 0-.19.25ZM13.55 13.9a1 1 0 0 0 1.34-.44c0-.02.02-.04.04-.06.03-.05.08-.13.15-.2.14-.13.28-.2.42-.2.14 0 .28.07.42.2a1.28 1.28 0 0 1 .19.25 1 1 0 0 0 1.78-.9L17 13l.9-.45-.01-.01-.01-.02a2.26 2.26 0 0 0-.14-.23 3.28 3.28 0 0 0-.4-.5c-.37-.36-.98-.79-1.84-.79-.86 0-1.47.43-1.83.8a3.28 3.28 0 0 0-.55.72v.02h-.01v.01L14 13l-.9-.45a1 1 0 0 0 .45 1.34Z"
/>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 21c5.52 0 10-1.86 10-6 0-5.59-2.8-10.07-4.26-11.67a1 1 0 1 0-1.48 1.34 14.8 14.8 0 0 1 2.35 3.86A10.23 10.23 0 0 0 12 6C9.47 6 7.15 7.02 5.4 8.53a14.8 14.8 0 0 1 2.34-3.86 1 1 0 1 0-1.48-1.34A18.65 18.65 0 0 0 2 15c0 4.14 4.48 6 10 6Zm0-12c3.87 0 7 2 7 4.2S15.87 17 12 17s-7-1.6-7-3.8C5 11 8.13 9 12 9Z"
/>
</PanelIcon>
);
/** Discord docs — Activities (appearance / creative shapes). */
export const LookFeelPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
d="M2.06 7.61c-.25.95.31 1.92 1.26 2.18l4.3 1.15c.94.25 1.91-.31 2.17-1.26l1.15-4.3c.25-.94-.31-1.91-1.26-2.17l-4.3-1.15c-.94-.25-1.91.31-2.17 1.26l-1.15 4.3ZM12.98 7.87a2 2 0 0 0 1.75 2.95H20a2 2 0 0 0 1.76-2.95l-2.63-4.83a2 2 0 0 0-3.51 0l-2.63 4.83ZM5.86 13.27a.89.89 0 0 1 1.28 0l.75.77a.9.9 0 0 0 .54.26l1.06.12c.5.06.85.52.8 1.02l-.13 1.08c-.02.2.03.42.14.6l.56.92c.27.43.14 1-.28 1.26l-.9.58a.92.92 0 0 0-.37.48l-.36 1.02a.9.9 0 0 1-1.15.57l-1-.36a.89.89 0 0 0-.6 0l-1 .36a.9.9 0 0 1-1.15-.57l-.36-1.02a.92.92 0 0 0-.37-.48l-.9-.58a.93.93 0 0 1-.28-1.26l.56-.93c.11-.17.16-.38.14-.59l-.12-1.08c-.06-.5.3-.96.8-1.02l1.05-.12a.9.9 0 0 0 .54-.26l.75-.77ZM18.52 13.71a1.1 1.1 0 0 0-2.04 0l-.46 1.24c-.19.5-.57.88-1.07 1.07l-1.24.46a1.1 1.1 0 0 0 0 2.04l1.24.46c.5.19.88.57 1.07 1.07l.46 1.24c.35.95 1.7.95 2.04 0l.46-1.24c.19-.5.57-.88 1.07-1.07l1.24-.46a1.1 1.1 0 0 0 0-2.04l-1.24-.46a1.8 1.8 0 0 1-1.07-1.07l-.46-1.24Z"
/>
</PanelIcon>
);
/** Discord docs — Help Center (behaviour / how the app acts). */
export const BehaviourPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm-.28-16c-.98 0-1.81.47-2.27 1.14A1 1 0 1 1 7.8 7.01 4.73 4.73 0 0 1 11.72 5c2.5 0 4.65 1.88 4.65 4.38 0 2.1-1.54 3.77-3.52 4.24l.14 1a1 1 0 0 1-1.98.27l-.28-2a1 1 0 0 1 .99-1.14c1.54 0 2.65-1.14 2.65-2.38 0-1.23-1.1-2.37-2.65-2.37ZM13 17.88a1.13 1.13 0 1 1-2.25 0 1.13 1.13 0 0 1 2.25 0Z"
/>
</PanelIcon>
);
/** Discord docs — Build Your First App rocket (power / performance). */
export const PowerPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
d="M7.8 15.77c.7.43 1.2 1.14 1.2 1.96V21a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3.27c0-.82.5-1.53 1.2-1.96a8.06 8.06 0 0 0 .12-13.63c-.6-.39-1.32.09-1.32.8v5.98a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V2.94c0-.71-.72-1.19-1.32-.8a8.06 8.06 0 0 0 .12 13.63Z"
/>
</PanelIcon>
);
/** Discord docs — Social Layer / Clyde (rich presence). */
export const RpcPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.97 4.06c0 .18.08.35.24.43.55.28.9.82 1.04 1.42.3 1.24.75 3.7.75 7.09v4.91a3.09 3.09 0 0 1-5.85 1.38l-1.76-3.51a1.09 1.09 0 0 0-1.23-.55c-.57.13-1.36.27-2.16.27s-1.6-.14-2.16-.27c-.49-.11-1 .1-1.23.55l-1.76 3.51A3.09 3.09 0 0 1 1 17.91V13c0-3.38.46-5.85.75-7.1.15-.6.49-1.13 1.04-1.4a.47.47 0 0 0 .24-.44c0-.7.48-1.32 1.2-1.47l2.93-.62c.5-.1 1 .06 1.36.4.35.34.78.71 1.28.68a42.4 42.4 0 0 1 4.4 0c.5.03.93-.34 1.28-.69.35-.33.86-.5 1.36-.39l2.94.62c.7.15 1.19.78 1.19 1.47ZM20 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM15.5 12a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 7a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2H7v1a1 1 0 1 1-2 0v-1H4a1 1 0 1 1 0-2h1V7Z"
/>
</PanelIcon>
);
/** Discord-style filled globe (networking / proxy). */
export const NetworkPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm6.92 6h-3.1a15.9 15.9 0 0 0-1.38-3.52A8.03 8.03 0 0 1 18.92 8ZM12 4c.7 0 1.86 1.35 2.55 4H9.45C10.14 5.35 11.3 4 12 4ZM4.26 14a7.98 7.98 0 0 1 0-4h3.27a17.4 17.4 0 0 0 0 4H4.26Zm1.82 2h3.1c.3 1.34.77 2.55 1.38 3.52A8.03 8.03 0 0 1 6.08 16ZM9.45 16h5.1c-.69 2.65-1.85 4-2.55 4s-1.86-1.35-2.55-4Zm5.29 3.52c.61-.97 1.08-2.18 1.38-3.52h3.1a8.03 8.03 0 0 1-4.48 3.52ZM16.47 14h3.27a7.98 7.98 0 0 0 0-4h-3.27a17.4 17.4 0 0 1 0 4ZM8.18 8H5.08a8.03 8.03 0 0 1 4.48-3.52C8.95 5.45 8.48 6.66 8.18 8Z"
/>
</PanelIcon>
);
/** Discord docs — Report Issues bug (advanced / debug). */
export const AdvancedPanelIcon = () => (
<PanelIcon>
<path
fill="currentColor"
d="M9.54 3.28a3 3 0 0 0-.37.74c-.19.52.28.98.83.98h4c.55 0 1.02-.46.83-.98a2.96 2.96 0 0 0-.37-.74c.23-.16.56-.28.99-.28a1 1 0 1 0 0-2c-1 0-2.01.4-2.68 1.1a3 3 0 0 0-1.54 0A3.76 3.76 0 0 0 8.55 1a1 1 0 0 0 0 2c.43 0 .76.12 1 .28ZM18.01 18.85c-.04.1-.03.22.05.3l1.51 1.5a1 1 0 0 1-1.41 1.42l-1.18-1.18a.26.26 0 0 0-.37 0 6.7 6.7 0 0 1-2.8 1.82c-.42.14-.83-.2-.86-.64l-.42-8.57a.53.53 0 0 0-1.05 0l-.43 8.57c-.03.45-.44.78-.87.64A6.7 6.7 0 0 1 7.4 20.9a.26.26 0 0 0-.37-.01l-1.18 1.18a1 1 0 0 1-1.41-1.41l1.51-1.51c.08-.08.1-.2.05-.3-.47-.94-.78-2-.92-3.12a.25.25 0 0 0-.25-.23H3a1 1 0 1 1 0-2h1.82c.13 0 .24-.1.25-.23.14-1.13.45-2.18.92-3.12a.25.25 0 0 0-.05-.3l-1.51-1.5a1 1 0 1 1 1.41-1.42L7.02 8.1c.1.1.27.1.37 0a6.66 6.66 0 0 1 2.95-1.87c.38-.11.75.14.85.52l.57 2.27c.06.25.42.25.48 0l.57-2.27c.1-.38.47-.63.85-.52 1.1.33 2.11.98 2.95 1.86.1.1.26.11.37.01l1.18-1.18a1 1 0 1 1 1.41 1.41l-1.51 1.51c-.08.08-.1.2-.05.3.47.94.78 2 .92 3.12.01.13.12.23.25.23H21a1 1 0 1 1 0 2h-1.82c-.13 0-.24.1-.25.23a9.68 9.68 0 0 1-.92 3.12Z"
/>
</PanelIcon>
);

View file

@ -1,12 +0,0 @@
.toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.tip {
margin-top: 8px;
color: var(--text-muted, var(--text-subtle));
max-width: 42rem;
line-height: 1.35;
}

View file

@ -1,49 +1,26 @@
import { For, Show } from "solid-js";
import type { Keybind } from "../../../@types/keybind.js";
import { EmptyState } from "../components/EmptyState.jsx";
import { KeybindCard } from "../components/KeybindCard.jsx";
import { KeybindMaker } from "../components/KeybindMaker.jsx";
import { SettingsPageHeader } from "../components/SettingsPageHeader.jsx";
import classes from "./KeybindsPage.module.css";
const {
plugin: { store },
ui: { Button, ButtonSizes, openModal, Text, TextTags },
ui: { Button, Header, HeaderTags, ButtonSizes, Divider, openModal },
} = shelter;
import { For } from "solid-js";
import type { Keybind } from "../../../@types/keybind.js";
import { KeybindCard } from "../components/KeybindCard.jsx";
import { KeybindMaker } from "../components/KeybindMaker.jsx";
export function KeybindsPage() {
function addNewKeybind() {
openModal(({ close }: { close: () => void }) => <KeybindMaker close={close} />);
}
const t = store.i18n;
const keybinds = () => (store.settings.keybinds as Keybind[]) ?? [];
return (
<>
<SettingsPageHeader title={t["keybinds-pageTitle"]} description={t["keybinds-pageDesc"]}>
<Text tag={TextTags.textSM} class={classes.tip}>
{t["keybind-globalNote"]}
</Text>
</SettingsPageHeader>
<div class={classes.toolbar}>
<Header tag={HeaderTags.H1}>Keybinds</Header>
<Divider mt mb />
<div style={{ display: "flex", "justify-content": "flex-end", "margin-bottom": "12px" }}>
<Button size={ButtonSizes.LARGE} onClick={addNewKeybind}>
{t["keybind-addKeybind"]}
Create a keybind
</Button>
</div>
<Show
when={keybinds().length > 0}
fallback={
<EmptyState
message={t["keybinds-empty"]}
description={t["keybinds-emptyDesc"]}
actionLabel={t["keybind-addKeybind"]}
onAction={addNewKeybind}
/>
}
>
<For each={keybinds()}>{(keybind: Keybind) => <KeybindCard keybind={keybind} />}</For>
</Show>
<For each={store.settings.keybinds}>{(keybind: Keybind) => <KeybindCard keybind={keybind} />}</For>
</>
);
}

View file

@ -1,6 +0,0 @@
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}

View file

@ -1,23 +1,14 @@
import { createSignal, For, onMount, Show } from "solid-js";
import type { LegcordPluginInfo } from "../../../@types/legcordWindow.js";
import { EmptyState } from "../components/EmptyState.jsx";
import { PluginCard } from "../components/PluginCard.jsx";
import { SettingsPageHeader } from "../components/SettingsPageHeader.jsx";
import classes from "./PluginsPage.module.css";
const {
ui: { Button, ButtonSizes, showToast },
plugin: { store },
ui: { Header, HeaderTags, Divider, Button, ButtonSizes, showToast },
} = shelter;
function formatPluginMessage(template: string, name: string) {
return template.replaceAll("{name}", name);
}
export function PluginsPage() {
const [plugins, setPlugins] = createSignal<LegcordPluginInfo[]>([]);
const [busyIds, setBusyIds] = createSignal<string[]>([]);
const t = () => store.i18n;
const setBusy = (pluginId: string, state: boolean) => {
setBusyIds((current) => {
@ -40,10 +31,9 @@ export function PluginsPage() {
try {
if (enabled && !plugin.compatible) {
showToast({
title: t()["plugins-toastTitle"],
title: "Plugins",
content:
plugin.compatibilityMessage ??
formatPluginMessage(t()["plugins-toastIncompatible"], plugin.name),
plugin.compatibilityMessage ?? `${plugin.name} is not compatible with this Legcord version.`,
duration: 3500,
});
return;
@ -51,11 +41,8 @@ export function PluginsPage() {
const result = await window.legcord.plugins.setEnabled(plugin.id, enabled);
if (!result.ok) {
showToast({
title: t()["plugins-toastTitle"],
content: formatPluginMessage(
enabled ? t()["plugins-toastEnableFailed"] : t()["plugins-toastDisableFailed"],
plugin.name,
),
title: "Plugins",
content: `Failed to ${enabled ? "enable" : "disable"} ${plugin.name}.`,
duration: 3000,
});
}
@ -70,11 +57,8 @@ export function PluginsPage() {
try {
const result = await window.legcord.plugins.reload(plugin.id);
showToast({
title: t()["plugins-toastTitle"],
content: formatPluginMessage(
result.ok ? t()["plugins-toastReloaded"] : t()["plugins-toastReloadFailed"],
plugin.name,
),
title: "Plugins",
content: result.ok ? `Reloaded ${plugin.name}.` : `Failed to reload ${plugin.name}.`,
duration: 2500,
});
await refreshPlugins();
@ -89,37 +73,29 @@ export function PluginsPage() {
return (
<>
<SettingsPageHeader title={t()["plugins-pageTitle"]} description={t()["plugins-pageDesc"]} />
<div class={classes.toolbar}>
<Header tag={HeaderTags.H1}>Plugins</Header>
<Divider mt mb />
<div style={{ display: "flex", gap: "8px", "margin-bottom": "12px" }}>
<Button size={ButtonSizes.LARGE} onClick={() => void refreshPlugins()}>
{t()["plugins-refresh"]}
Refresh List
</Button>
<Button size={ButtonSizes.LARGE} onClick={window.legcord.plugins.openFolder}>
{t()["plugins-openFolder"]}
Open Plugins Folder
</Button>
</div>
<Show
when={plugins().length > 0}
fallback={
<EmptyState
message={t()["plugins-empty"]}
description={t()["plugins-emptyDesc"]}
actionLabel={t()["plugins-openFolder"]}
onAction={window.legcord.plugins.openFolder}
/>
}
>
<For each={plugins()}>
{(plugin) => (
<PluginCard
plugin={plugin}
busy={isBusy(plugin.id)}
onToggle={(enabled) => void onToggle(plugin, enabled)}
onReload={() => void onReload(plugin)}
/>
)}
</For>
<Show when={plugins().length === 0}>
<Header tag={HeaderTags.H5}>No runtime plugins found in your plugins folder.</Header>
</Show>
<For each={plugins()}>
{(plugin) => (
<PluginCard
plugin={plugin}
busy={isBusy(plugin.id)}
onToggle={(enabled) => void onToggle(plugin, enabled)}
onReload={() => void onReload(plugin)}
/>
)}
</For>
</>
);
}

View file

@ -5,8 +5,6 @@ import { sleep } from "../../../common/sleep.js";
import { AddDetectableModal } from "../components/AddDetectableModal.jsx";
import { DetectableCard } from "../components/DetectableCard.jsx";
import { Dropdown } from "../components/Dropdown.jsx";
import { EmptyState } from "../components/EmptyState.jsx";
import { SettingsPageHeader } from "../components/SettingsPageHeader.jsx";
import classes from "./RegisteredGames.module.css";
const {
@ -95,7 +93,8 @@ export function RegisteredGamesPage() {
return (
<>
<SettingsPageHeader title={t["games-registeredGames"]} description={t["games-pageDesc"]} />
<Header tag={HeaderTags.H1}>{t["games-registeredGames"]}</Header>
<Divider mt mb />
<div class={classes.addBox}>
<Dropdown
class={classes.dropdown}
@ -129,21 +128,27 @@ export function RegisteredGamesPage() {
</div>
<Show
when={(detectables()?.length ?? 0) > 0}
fallback={<EmptyState message={t["games-empty"]} description={t["games-emptyDesc"]} />}
fallback={
<Header tag={HeaderTags.H5} class={classes.empty}>
{t["games-empty"]}
</Header>
}
>
<For each={detectables()}>
{(detectable) => <DetectableCard detectable={detectable} onRemove={refreshDetectables} />}
</For>
</Show>
<Header tag={HeaderTags.HeadingLG} class={classes.sectionHeader}>
<Header tag={HeaderTags.H3} class={classes.sectionHeader}>
{t["games-lastDetected"]}
</Header>
<Divider mt mb />
<Show
when={lastDetected().length > 0}
fallback={
<EmptyState message={t["games-lastDetectedEmpty"]} description={t["games-lastDetectedEmptyDesc"]} />
<Header tag={HeaderTags.H5} class={classes.empty}>
{t["games-lastDetectedEmpty"]}
</Header>
}
>
<ul class={classes.gameList}>
@ -166,14 +171,16 @@ export function RegisteredGamesPage() {
</ul>
</Show>
<Header tag={HeaderTags.HeadingLG} class={classes.sectionHeader}>
<Header tag={HeaderTags.H3} class={classes.sectionHeader}>
{t["games-blacklisted"]}
</Header>
<Divider mt mb />
<Show
when={blacklisted().length > 0}
fallback={
<EmptyState message={t["games-blacklistedEmpty"]} description={t["games-blacklistedEmptyDesc"]} />
<Header tag={HeaderTags.H5} class={classes.empty}>
{t["games-blacklistedEmpty"]}
</Header>
}
>
<ul class={classes.gameList}>

View file

@ -1,28 +1,3 @@
.search {
margin: 8px 0 4px;
}
.searchHint {
margin: 0 0 8px;
color: var(--text-muted, var(--text-subtle));
font-size: 12px;
line-height: 16px;
}
.noResults {
margin: 24px 0;
text-align: center;
color: var(--text-muted, var(--text-subtle));
font-size: 14px;
}
.panelActions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.crashTitle {
.category {
margin-bottom: 8px;
}

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,14 @@
import { createSignal, For, Show } from "solid-js";
import { createSignal, For } from "solid-js";
import type { Settings } from "../../../@types/settings.js";
import type { ThemeManifest } from "../../../@types/themeManifest.js";
import { EmptyState } from "../components/EmptyState.jsx";
import { SettingsPageHeader } from "../components/SettingsPageHeader.jsx";
import { ThemesCard } from "../components/ThemesCard.jsx";
import { refreshThemes, setConfig } from "../settings.js";
import classes from "./ThemesPages.module.css";
const {
ui: { Button, Header, HeaderTags, ButtonSizes, TextBox, showToast, SwitchItem, Text, TextTags },
ui: { Button, Header, HeaderTags, ButtonSizes, TextBox, showToast, SwitchItem },
plugin: { store },
} = shelter;
const BETTERDISCORD_THEMES_URL = "https://betterdiscord.app/themes";
export function ThemesPage() {
const [downloadUrl, setDownloadUrl] = createSignal("");
refreshThemes();
@ -33,11 +28,9 @@ export function ThemesPage() {
const settings = () => store.settings as Settings;
const t = store.i18n;
const themes = () => (store.themes as ThemeManifest[]) ?? [];
return (
<>
<SettingsPageHeader title={t["themes-pageTitle"]} description={t["themes-pageDesc"]} divider={true} />
<Header tag={HeaderTags.H1}>Themes</Header>
<SwitchItem
note={store.i18n["settings-quickCss-desc"]}
value={settings().quickCss}
@ -73,7 +66,7 @@ export function ThemesPage() {
store.themes = window.legcord.themes.refresh();
}}
>
{t["themes-refresh"]}
{t["themes-refresh"] ?? "Refresh"}
</Button>
</div>
<div class={classes.addBox}>
@ -86,24 +79,9 @@ export function ThemesPage() {
{t["themes-import"]}
</Button>
</div>
<Text tag={TextTags.textSM} class={classes.importNote}>
{t["themes-importUrlNote"]}
</Text>
<hr class={classes.divider} />
<Header tag={HeaderTags.HeadingLG}>{t["themes-installed"]}</Header>
<Show
when={themes().length > 0}
fallback={
<EmptyState
message={t["themes-empty"]}
description={t["themes-emptyDesc"]}
linkHref={BETTERDISCORD_THEMES_URL}
linkLabel={t["themes-browseThemes"]}
/>
}
>
<For each={themes()}>{(theme: ThemeManifest) => <ThemesCard theme={theme} />}</For>
</Show>
<Header tag={HeaderTags.H3}>{t["themes-installed"]}</Header>
<For each={store.themes}>{(theme: ThemeManifest) => <ThemesCard theme={theme} />}</For>
</>
);
}

View file

@ -21,8 +21,3 @@
border: none;
border-top: 1px solid var(--background-modifier-accent);
}
.importNote {
margin-top: 6px;
color: var(--text-muted, var(--text-subtle));
}

View file

@ -18,75 +18,14 @@ export function refreshThemes() {
store.themes = window.legcord.themes.getThemes();
}
/** Nearest scrollable ancestor — Discord's settings content scroller. */
function findScrollableAncestor(start: Element | null | undefined): HTMLElement | null {
let el = start instanceof HTMLElement ? start : (start?.parentElement ?? null);
while (el) {
const { overflowY } = getComputedStyle(el);
if (
(overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") &&
el.scrollHeight > el.clientHeight + 1
) {
return el;
}
el = el.parentElement;
}
return null;
}
function findSettingsScroller(): HTMLElement | null {
// Prefer a scroller that currently contains focus / active control.
const fromFocus = findScrollableAncestor(document.activeElement);
if (fromFocus) return fromFocus;
for (const root of document.querySelectorAll("shltr-rroot")) {
const found = findScrollableAncestor(root.parentElement);
if (found) return found;
}
return null;
}
/** Keep Discord's settings scroller put across store/DOM updates. */
export function withPreservedSettingsScroll<T>(run: () => T): T {
const scroller = findSettingsScroller();
const top = scroller?.scrollTop ?? 0;
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
const result = run();
if (!scroller) return result;
const restore = () => {
if (scroller.scrollTop !== top) scroller.scrollTop = top;
// Re-focus without scrolling — Discord/React remounts can move focus to the top.
if (active && document.contains(active) && document.activeElement !== active) {
try {
active.focus({ preventScroll: true });
} catch {
/* ignore */
}
}
};
restore();
queueMicrotask(restore);
requestAnimationFrame(() => {
restore();
requestAnimationFrame(restore);
});
// Late layout passes (Switch remounts, panel height changes).
setTimeout(restore, 0);
setTimeout(restore, 50);
return result;
}
export function setConfig<K extends keyof Settings>(key: K, value: Settings[K], shouldRestart?: boolean) {
withPreservedSettingsScroll(() => {
// Mutate in place — do NOT replace store.settings (that remounts the page and jumps scroll).
store.settings[key] = value;
console.log(key, ":", store.settings[key]);
if (shouldRestart) {
isRestartRequired = true;
}
window.legcord.settings.setConfig(key, value);
});
store.settings[key] = value;
console.log(key, ":", store.settings[key]);
if (shouldRestart) {
isRestartRequired = true;
}
window.legcord.settings.setConfig(key, value);
refreshSettings();
}
function removeMod(array: ValidMods[], filter: ValidMods) {
@ -95,12 +34,14 @@ function removeMod(array: ValidMods[], filter: ValidMods) {
export function toggleMod(mod: ValidMods, enabled: boolean) {
isRestartRequired = true;
const currentMods = store.settings.mods as ValidMods[];
const currentMods = store.settings.mods;
if (enabled) {
if (mod === "vencord") {
setConfig("mods", [...removeMod(currentMods, "equicord"), "vencord"]);
currentMods.push("vencord");
setConfig("mods", removeMod(currentMods, "equicord"));
} else if (mod === "equicord") {
setConfig("mods", [...removeMod(currentMods, "vencord"), "equicord"]);
currentMods.push("equicord");
setConfig("mods", removeMod(currentMods, "vencord"));
}
} else {
setConfig("mods", removeMod(currentMods, mod));

View file

@ -66,25 +66,19 @@ function layerPop() {
export function onLoad() {
log("Legcord Titlebar Controller");
if (settings.windowStyle === "default") {
document.body.setAttribute("customTitlebar", "");
injectButtonControls();
return;
switch (settings.windowStyle) {
case "default":
document.body.setAttribute("customTitlebar", "");
injectButtonControls();
break;
case "overlay":
document.body.setAttribute("customTitlebar", "");
dispatcher.subscribe("LAYER_PUSH", layerPush);
dispatcher.subscribe("LAYER_POP", layerPop);
break;
default:
log("Unsupported window style");
}
// Native + transparency on macOS uses the same overlay chrome as "overlay" (Legcord#1095).
const overlayLike =
settings.windowStyle === "overlay" ||
(settings.windowStyle === "native" && window.legcord.platform === "darwin" && settings.transparency !== "none");
if (overlayLike) {
document.body.setAttribute("customTitlebar", "");
dispatcher.subscribe("LAYER_PUSH", layerPush);
dispatcher.subscribe("LAYER_POP", layerPop);
return;
}
log("Unsupported window style");
}
export function onUnload() {

128
src/splash/splash.css Normal file
View file

@ -0,0 +1,128 @@
/*MIT License
Copyright (c) 2022 GooseMod
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.*/
:root {
--background-primary: #282b30;
--background-secondary: rgba(255, 255, 255, 0.1);
--brand-experiment: #7289da;
--header-primary: #fff;
--text-muted: #72767d;
}
@font-face {
font-family: Whitney;
font-weight: 400;
font-style: normal;
src: url(https://legcord.app/whitney_400.woff) format("woff");
}
html,
body {
-webkit-app-region: drag;
overflow: hidden;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: var(--background-primary);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
* {
font-family: "Whitney", sans-serif;
box-sizing: border-box;
-webkit-user-select: none;
user-select: none;
cursor: default;
}
video {
display: block;
width: 200px;
height: 150px;
object-fit: cover;
margin: 0 auto;
}
#text-splashscreen {
font-size: 16px;
text-align: center;
color: var(--header-primary);
font-weight: 400;
font-style: italic;
text-transform: uppercase;
width: 100%;
}
#bar-container,
#bar-fill {
width: 180px;
height: 8px;
border-radius: 4px;
visibility: hidden;
}
#bar-container {
background-color: var(--background-secondary);
position: relative;
margin-top: 12px;
}
#bar-fill {
background-color: var(--brand-experiment);
width: 0;
}
#debug {
position: absolute;
bottom: 6px;
right: 6px;
text-align: right;
font-size: 10px;
color: var(--text-muted);
white-space: pre;
}
img.logo {
width: 272px;
}
button {
background: var(--brand-experiment);
color: var(--header-primary);
outline: none;
border: none;
border-radius: 5px;
padding: 8px;
-webkit-user-select: all;
user-select: all;
margin: 0 auto;
margin-top: 10px;
-webkit-app-region: no-drag;
transition: 0.17s ease;
display: none;
}
button:hover {
cursor: grab;
}

View file

@ -3,6 +3,9 @@
<head>
<meta charset="UTF-8" />
<title>Legcord</title>
<style>
@import url("../css/splash.css");
</style>
<script>
window.onbeforeunload = () => {
const style = document.createElement("style");
@ -10,137 +13,6 @@
document.head.append(style);
};
</script>
<style>
/*MIT License
Copyright (c) 2022 GooseMod
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.*/
:root {
--background-primary: #282b30;
--background-secondary: rgba(255, 255, 255, 0.1);
--brand-experiment: #7289da;
--header-primary: #fff;
--text-muted: #72767d;
}
@font-face {
font-family: Whitney;
font-weight: 400;
font-style: normal;
src: url(https://legcord.app/whitney_400.woff) format("woff");
}
html,
body {
-webkit-app-region: drag;
overflow: hidden;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: var(--background-primary);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
* {
font-family: "Whitney", sans-serif;
box-sizing: border-box;
-webkit-user-select: none;
user-select: none;
cursor: default;
}
video {
display: block;
width: 200px;
height: 150px;
object-fit: cover;
margin: 0 auto;
}
#text-splashscreen {
font-size: 16px;
text-align: center;
color: var(--header-primary);
font-weight: 400;
font-style: italic;
text-transform: uppercase;
width: 100%;
}
#bar-container,
#bar-fill {
width: 180px;
height: 8px;
border-radius: 4px;
visibility: hidden;
}
#bar-container {
background-color: var(--background-secondary);
position: relative;
margin-top: 12px;
}
#bar-fill {
background-color: var(--brand-experiment);
width: 0;
}
#debug {
position: absolute;
bottom: 6px;
right: 6px;
text-align: right;
font-size: 10px;
color: var(--text-muted);
white-space: pre;
}
img.logo {
width: 272px;
}
button {
background: var(--brand-experiment);
color: var(--header-primary);
outline: none;
border: none;
border-radius: 5px;
padding: 8px;
-webkit-user-select: all;
user-select: all;
margin: 0 auto;
margin-top: 10px;
-webkit-app-region: no-drag;
transition: 0.17s ease;
display: none;
}
button:hover {
cursor: grab;
}
</style>
</head>
<body>