mirror of
https://github.com/smartfrigde/armcord.git
synced 2026-08-19 14:23:23 +00:00
Compare commits
17 commits
99281f3f05
...
642533f800
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642533f800 | ||
|
|
6ac0f48f4f | ||
|
|
d5641b5f0e | ||
|
|
38e5b20dfe | ||
|
|
f0ff4f9221 | ||
|
|
4fb59f7fff | ||
|
|
0599810505 | ||
|
|
853f3ff5e0 | ||
|
|
4e9da71366 | ||
|
|
bb7bf89312 | ||
|
|
464a1c2fb7 | ||
|
|
5d72d180b1 | ||
|
|
550662ce0c | ||
|
|
4ec80d11bb | ||
|
|
91a3d86a12 | ||
|
|
f30dadcdfd | ||
|
|
80689ea23b |
61 changed files with 3221 additions and 885 deletions
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -23,6 +23,13 @@
|
||||||
- [ ] Manual verification for affected flows
|
- [ ] Manual verification for affected flows
|
||||||
- [ ] Screenshots or recordings for UI changes
|
- [ ] 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
|
## Notes
|
||||||
|
|
||||||
<!-- Add links to related issues, screenshots, follow-up work, or implementation details. -->
|
<!-- Add links to related issues, screenshots, follow-up work, or implementation details. -->
|
||||||
|
|
|
||||||
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
// 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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -98,6 +98,11 @@ Before requesting review, verify:
|
||||||
- Reference related issues and prior discussions.
|
- Reference related issues and prior discussions.
|
||||||
- If you added a workaround/suppression, include the reason in code comments and in the PR description.
|
- 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
|
## Documentation
|
||||||
|
|
||||||
We are still improving project and codebase documentation.
|
We are still improving project and codebase documentation.
|
||||||
|
|
|
||||||
52
assets/app/css/inviteBack.css
Normal file
52
assets/app/css/inviteBack.css
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,12 @@
|
||||||
"settings-prfmMode": "Leistungsmodus",
|
"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-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-performance": "Leistung",
|
||||||
|
"settings-prfmMode-balanced": "Ausgewogen",
|
||||||
"settings-prfmMode-battery": "Batterie",
|
"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-dynamic": "Dynamisch",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Automatische Verstärkung deaktivieren",
|
"settings-disableAutogain": "Automatische Verstärkung deaktivieren",
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,11 @@
|
||||||
"settings-hardwareAcceleration": "Hardware acceleration",
|
"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-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": "Process scanning",
|
||||||
"settings-processScanning-desc": "Enables scanning for running games to improve Rich Presence detection.",
|
"settings-processScanning-desc": "Scan for running games to improve Rich Presence detection. Disable to reduce CPU usage. Requires a restart.",
|
||||||
"settings-windowsLegacyScanning": "Windows legacy scanning",
|
"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.",
|
"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-scanInterval": "Scan interval (ms)",
|
"settings-scanInterval": "Scan interval (ms)",
|
||||||
"settings-scanInterval-desc": "Sets how often (in milliseconds) the process scanning occurs. Lower values may improve detection speed but can increase CPU usage.",
|
"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-blockPowerSavingInVoiceChat": "Block power saving in voice chat",
|
"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-blockPowerSavingInVoiceChat-desc": "Prevent Legcord from being suspended. Keeps system active but allows screen to be turned off.",
|
||||||
"settings-mobileMode": "Mobile mode",
|
"settings-mobileMode": "Mobile mode",
|
||||||
|
|
@ -115,7 +115,12 @@
|
||||||
"settings-prfmMode": "Performance mode",
|
"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-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-performance": "Performance",
|
||||||
|
"settings-prfmMode-balanced": "Balanced",
|
||||||
"settings-prfmMode-battery": "Battery",
|
"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-dynamic": "Dynamic",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Disable autogain",
|
"settings-disableAutogain": "Disable autogain",
|
||||||
|
|
@ -142,6 +147,7 @@
|
||||||
"settings-save": "Save Settings",
|
"settings-save": "Save Settings",
|
||||||
"settings-experimental": "Experimental",
|
"settings-experimental": "Experimental",
|
||||||
"settings-restart": "Restart App",
|
"settings-restart": "Restart App",
|
||||||
|
"invite-goBackToApp": "Go back to the Discord app",
|
||||||
"settings-updater": "Check for updates",
|
"settings-updater": "Check for updates",
|
||||||
"settings-skipSplash": "Skip Splash Screen",
|
"settings-skipSplash": "Skip Splash Screen",
|
||||||
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
|
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
|
||||||
|
|
@ -156,10 +162,39 @@
|
||||||
"settings-quickCss": "Quick CSS",
|
"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-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": "Look and feel",
|
||||||
|
"settings-category-lookAndFeel-desc": "Window chrome, tray icon, splash screen, and mobile layout.",
|
||||||
"settings-category-mods": "Mods",
|
"settings-category-mods": "Mods",
|
||||||
|
"settings-category-mods-desc": "Client mods, CSP, and plugin capabilities.",
|
||||||
"settings-category-behaviour": "Behaviour",
|
"settings-category-behaviour": "Behaviour",
|
||||||
|
"settings-category-behaviour-desc": "Startup, tray, scrolling, spellcheck, and Discord channel.",
|
||||||
"settings-category-legacy": "Legacy features",
|
"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-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-about": "About Legcord",
|
||||||
"menu-developerTools": "Developer tools",
|
"menu-developerTools": "Developer tools",
|
||||||
"menu-openSettings": "Open settings",
|
"menu-openSettings": "Open settings",
|
||||||
|
|
@ -243,8 +278,6 @@
|
||||||
"settings-channel-stable": "Stable",
|
"settings-channel-stable": "Stable",
|
||||||
"settings-channel-canary": "Canary",
|
"settings-channel-canary": "Canary",
|
||||||
"settings-channel-ptb": "PTB",
|
"settings-channel-ptb": "PTB",
|
||||||
"settings-category-powerManagement": "Power Management",
|
|
||||||
"settings-category-arrpc": "arRPC",
|
|
||||||
"settings-extendedPluginAbilities": "Extended plugin abilities",
|
"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-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",
|
"settings-audio-loopback": "Loopback",
|
||||||
|
|
@ -276,12 +309,21 @@
|
||||||
"detectable-appName": "App Name*",
|
"detectable-appName": "App Name*",
|
||||||
"detectable-appId": "App ID*",
|
"detectable-appId": "App ID*",
|
||||||
"detectable-themes": "Themes",
|
"detectable-themes": "Themes",
|
||||||
|
"detectable-themes-note": "Optional genre tags for this game, comma-separated.",
|
||||||
"detectable-aliases": "Aliases",
|
"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 game’s rich presence.",
|
||||||
"detectable-enabled": "Enabled",
|
"detectable-enabled": "Enabled",
|
||||||
"detectable-placeholderName": "e.g. Discord",
|
"detectable-placeholderName": "e.g. Discord",
|
||||||
"detectable-placeholderId": "e.g. 1234567890",
|
"detectable-placeholderId": "e.g. 1234567890",
|
||||||
"detectable-placeholderThemes": "Action, Adventure",
|
"detectable-placeholderThemes": "Action, Adventure",
|
||||||
"detectable-placeholderAliases": "Alias1, Alias2",
|
"detectable-placeholderAliases": "Alias1, Alias2",
|
||||||
|
"themes-pageTitle": "Themes",
|
||||||
|
"themes-pageDesc": "Install BetterDiscord themes or write Quick CSS to customize Discord’s 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-success": "Success!",
|
||||||
"themes-updated": "Theme successfully updated!",
|
"themes-updated": "Theme successfully updated!",
|
||||||
"themes-bdInstalled": "BD theme successfully installed!",
|
"themes-bdInstalled": "BD theme successfully installed!",
|
||||||
|
|
@ -294,23 +336,48 @@
|
||||||
"themes-importFromFile": "Import from file",
|
"themes-importFromFile": "Import from file",
|
||||||
"themes-openThemesFolder": "Open themes folder",
|
"themes-openThemesFolder": "Open themes folder",
|
||||||
"themes-import": "Import",
|
"themes-import": "Import",
|
||||||
|
"themes-refresh": "Refresh",
|
||||||
"themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]",
|
"themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]",
|
||||||
"themes-installed": "Installed Themes",
|
"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-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-refreshList": "Refresh list",
|
||||||
"games-add": "Add",
|
"games-add": "Add",
|
||||||
"games-remove": "Remove",
|
"games-remove": "Remove",
|
||||||
"games-removeConfirmHeader": "Remove game?",
|
"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-removeConfirmBody": "This game will be removed from rich presence. The client must be restarted for changes to take effect.",
|
||||||
"games-noExecutables": "No executables",
|
"games-noExecutables": "No executables",
|
||||||
"games-empty": "No registered games. Add one using the dropdown above.",
|
"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-lastDetected": "Last detected games",
|
"games-lastDetected": "Last detected games",
|
||||||
"games-lastDetectedEmpty": "No games detected yet. Start a game with rich presence to see them here.",
|
"games-lastDetectedEmpty": "No games detected yet.",
|
||||||
|
"games-lastDetectedEmptyDesc": "Start a game with rich presence while Legcord is running to see it listed here.",
|
||||||
"games-blacklist": "Blacklist",
|
"games-blacklist": "Blacklist",
|
||||||
"games-blacklisted": "Blacklisted games",
|
"games-blacklisted": "Blacklisted games",
|
||||||
"games-blacklistedEmpty": "No blacklisted games.",
|
"games-blacklistedEmpty": "No blacklisted games.",
|
||||||
|
"games-blacklistedEmptyDesc": "Games you blacklist won’t show up in last detected.",
|
||||||
"games-removeFromBlacklist": "Remove",
|
"games-removeFromBlacklist": "Remove",
|
||||||
"games-application": "Application",
|
"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-selectSource": "Please select a source",
|
||||||
"screenshare-venmicDisabled": "Venmic disabled",
|
"screenshare-venmicDisabled": "Venmic disabled",
|
||||||
"screenshare-share": "Share",
|
"screenshare-share": "Share",
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,12 @@
|
||||||
"settings-prfmMode": "Modo rendimiento",
|
"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-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-performance": "Rendimiento",
|
||||||
|
"settings-prfmMode-balanced": "Equilibrado",
|
||||||
"settings-prfmMode-battery": "Batería",
|
"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-dynamic": "Dinámico",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Desactivar ganancia automática",
|
"settings-disableAutogain": "Desactivar ganancia automática",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,12 @@
|
||||||
"settings-prfmMode": "Mode performance",
|
"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-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-performance": "Performance",
|
||||||
|
"settings-prfmMode-balanced": "Équilibré",
|
||||||
"settings-prfmMode-battery": "Batterie",
|
"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-dynamic": "Dynamique",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableHttpCache": "Désactiver le cache HTTP",
|
"settings-disableHttpCache": "Désactiver le cache HTTP",
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,12 @@
|
||||||
"settings-prfmMode": "Teljesítmény mód",
|
"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-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-performance": "Teljesítmény",
|
||||||
|
"settings-prfmMode-balanced": "Kiegyensúlyozott",
|
||||||
"settings-prfmMode-battery": "Akkumulátor",
|
"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-dynamic": "Dinamikus",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Automatikus erősítés letiltása",
|
"settings-disableAutogain": "Automatikus erősítés letiltása",
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,12 @@
|
||||||
"settings-prfmMode": "Mode kinerja",
|
"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-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-performance": "Kinerja",
|
||||||
|
"settings-prfmMode-balanced": "Seimbang",
|
||||||
"settings-prfmMode-battery": "Baterai",
|
"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-dynamic": "Dinamis",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Nonaktifkan autogain",
|
"settings-disableAutogain": "Nonaktifkan autogain",
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,12 @@
|
||||||
"settings-prfmMode": "Modo de performance",
|
"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-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-performance": "Performance",
|
||||||
|
"settings-prfmMode-balanced": "Equilibrado",
|
||||||
"settings-prfmMode-battery": "Bateria",
|
"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-dynamic": "Dinâmico",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Desabilitar ganho automático",
|
"settings-disableAutogain": "Desabilitar ganho automático",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,12 @@
|
||||||
"settings-audio-desc": "Selecione o método que o Legcord usa para capturar o áudio do seu sistema durante o partilhamento de ecrã.",
|
"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": "Modo de desempenho",
|
||||||
"settings-prfmMode-performance": "Desempenho",
|
"settings-prfmMode-performance": "Desempenho",
|
||||||
|
"settings-prfmMode-balanced": "Equilibrado",
|
||||||
"settings-prfmMode-dynamic": "Dinâmico",
|
"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-none": "Nenjum",
|
||||||
"settings-save": "Gravar configurações",
|
"settings-save": "Gravar configurações",
|
||||||
"settings-restart": "Reiniciar a app",
|
"settings-restart": "Reiniciar a app",
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,12 @@
|
||||||
"settings-prfmMode": "Режим Производительности",
|
"settings-prfmMode": "Режим Производительности",
|
||||||
"settings-prfmMode-desc": "Режимы Производительности - экспериментальная функция Legcord, оптимизирующая производительность и скорость работы программы под ваши нужды. Эффект может зависеть от вашей системной конфигурации и паттернов использования, поэтому мы рекомендуем вам попробовать каждый режим и выбрать тот, что лучше всего подходит именно вам.",
|
"settings-prfmMode-desc": "Режимы Производительности - экспериментальная функция Legcord, оптимизирующая производительность и скорость работы программы под ваши нужды. Эффект может зависеть от вашей системной конфигурации и паттернов использования, поэтому мы рекомендуем вам попробовать каждый режим и выбрать тот, что лучше всего подходит именно вам.",
|
||||||
"settings-prfmMode-performance": "Производительный",
|
"settings-prfmMode-performance": "Производительный",
|
||||||
|
"settings-prfmMode-balanced": "Сбалансированный",
|
||||||
"settings-prfmMode-battery": "Энергосбережение",
|
"settings-prfmMode-battery": "Энергосбережение",
|
||||||
|
"settings-prfmMode-memory": "Экономия памяти",
|
||||||
|
"settings-prfmMode-voip": "Голос и видео",
|
||||||
|
"settings-prfmMode-latency": "Низкая задержка",
|
||||||
|
"settings-prfmMode-smoothScreenshare": "Плавная демонстрация экрана",
|
||||||
"settings-prfmMode-dynamic": "Динамический",
|
"settings-prfmMode-dynamic": "Динамический",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Выключить автоусиление",
|
"settings-disableAutogain": "Выключить автоусиление",
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,12 @@
|
||||||
"settings-prfmMode": "Режим продуктивності",
|
"settings-prfmMode": "Режим продуктивності",
|
||||||
"settings-prfmMode-desc": "Режим продуктивності – це експериментальна функція в Legcord, розроблена для оптимізації швидкості реагування та продуктивності відповідно до ваших потреб. Вплив може відрізнятися залежно від вашого обладнання та використання, тому ми рекомендуємо вам спробувати кожен режим, щоб визначити, який найкраще підходить саме вам.",
|
"settings-prfmMode-desc": "Режим продуктивності – це експериментальна функція в Legcord, розроблена для оптимізації швидкості реагування та продуктивності відповідно до ваших потреб. Вплив може відрізнятися залежно від вашого обладнання та використання, тому ми рекомендуємо вам спробувати кожен режим, щоб визначити, який найкраще підходить саме вам.",
|
||||||
"settings-prfmMode-performance": "Продуктивність",
|
"settings-prfmMode-performance": "Продуктивність",
|
||||||
|
"settings-prfmMode-balanced": "Збалансований",
|
||||||
"settings-prfmMode-battery": "Батарея",
|
"settings-prfmMode-battery": "Батарея",
|
||||||
|
"settings-prfmMode-memory": "Економія пам'яті",
|
||||||
|
"settings-prfmMode-voip": "Голос і відео",
|
||||||
|
"settings-prfmMode-latency": "Низька затримка",
|
||||||
|
"settings-prfmMode-smoothScreenshare": "Плавна демонстрація екрана",
|
||||||
"settings-prfmMode-dynamic": "Динамічний",
|
"settings-prfmMode-dynamic": "Динамічний",
|
||||||
"settings-prfmMode-vaapi": "VAAPI",
|
"settings-prfmMode-vaapi": "VAAPI",
|
||||||
"settings-disableAutogain": "Вимкнути автоматичне посилення",
|
"settings-disableAutogain": "Вимкнути автоматичне посилення",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,24 @@
|
||||||
import type { Configuration } from "electron-builder";
|
import type { Configuration } from "electron-builder";
|
||||||
|
|
||||||
import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
|
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 = {
|
export const config: Configuration = {
|
||||||
appId: "app.legcord.Legcord",
|
appId: "app.legcord.Legcord",
|
||||||
|
|
@ -43,6 +61,19 @@ 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: {
|
nsis: {
|
||||||
oneClick: false,
|
oneClick: false,
|
||||||
allowToChangeInstallationDirectory: true,
|
allowToChangeInstallationDirectory: true,
|
||||||
|
|
@ -71,6 +102,12 @@ export const config: Configuration = {
|
||||||
category: "Network",
|
category: "Network",
|
||||||
icon: "build/icon.icns",
|
icon: "build/icon.icns",
|
||||||
depends: ["libgbm-dev", "libasound2", "libnspr4", "libnss3"],
|
depends: ["libgbm-dev", "libasound2", "libnspr4", "libnss3"],
|
||||||
|
desktop: {
|
||||||
|
entry: {
|
||||||
|
Actions: availableActions,
|
||||||
|
},
|
||||||
|
desktopActions: desktopActions("/opt/Legcord/legcord"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
files: [
|
files: [
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
"@xmldom/xmldom": "^0.9.10",
|
"@xmldom/xmldom": "^0.9.10",
|
||||||
"app-builder-lib": "^26.8.1",
|
"app-builder-lib": "^26.8.1",
|
||||||
"babel-preset-solid": "^1.9.12",
|
"babel-preset-solid": "^1.9.12",
|
||||||
"electron": "43.1.1",
|
"electron": "43.2.0",
|
||||||
"electron-builder": "26.8.1",
|
"electron-builder": "26.8.1",
|
||||||
"lucide-solid": "^1.16.0",
|
"lucide-solid": "^1.16.0",
|
||||||
"rolldown": "1.0.3",
|
"rolldown": "1.0.3",
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
"xml-formatter": "^3.7.0"
|
"xml-formatter": "^3.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jellybrick/dbus-next": "^0.11.1",
|
||||||
"adm-zip": "^0.6.0",
|
"adm-zip": "^0.6.0",
|
||||||
"arrpc": "github:Legcord/arrpc#efe7589762470d32b9ba10d529be5acc23cd0e19",
|
"arrpc": "github:Legcord/arrpc#efe7589762470d32b9ba10d529be5acc23cd0e19",
|
||||||
"electron-context-menu": "^4.1.2",
|
"electron-context-menu": "^4.1.2",
|
||||||
|
|
|
||||||
198
pnpm-lock.yaml
generated
198
pnpm-lock.yaml
generated
|
|
@ -11,6 +11,9 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@jellybrick/dbus-next':
|
||||||
|
specifier: ^0.11.1
|
||||||
|
version: 0.11.1
|
||||||
adm-zip:
|
adm-zip:
|
||||||
specifier: ^0.6.0
|
specifier: ^0.6.0
|
||||||
version: 0.6.0
|
version: 0.6.0
|
||||||
|
|
@ -71,10 +74,10 @@ importers:
|
||||||
version: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1)
|
version: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1)
|
||||||
babel-preset-solid:
|
babel-preset-solid:
|
||||||
specifier: ^1.9.12
|
specifier: ^1.9.12
|
||||||
version: 1.9.12(@babel/core@8.0.0-rc.6)(solid-js@1.9.14)
|
version: 1.9.12(@babel/core@7.29.7)(solid-js@1.9.14)
|
||||||
electron:
|
electron:
|
||||||
specifier: 43.1.1
|
specifier: 43.2.0
|
||||||
version: 43.1.1
|
version: 43.2.0
|
||||||
electron-builder:
|
electron-builder:
|
||||||
specifier: 26.8.1
|
specifier: 26.8.1
|
||||||
version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
|
version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
|
||||||
|
|
@ -113,6 +116,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@babel/code-frame@8.0.0-rc.6':
|
||||||
resolution: {integrity: sha512-Ru0EdYEptXbJGAGDMsenx+RcelHazuj8spqi7l0geXEPXv0X7qVisqWX+2pMaEZsWhS3Q6Um7oITwhnLe0cHJQ==}
|
resolution: {integrity: sha512-Ru0EdYEptXbJGAGDMsenx+RcelHazuj8spqi7l0geXEPXv0X7qVisqWX+2pMaEZsWhS3Q6Um7oITwhnLe0cHJQ==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
@ -137,6 +144,10 @@ packages:
|
||||||
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
|
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@babel/generator@8.0.0-rc.6':
|
||||||
resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==}
|
resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
@ -163,6 +174,10 @@ packages:
|
||||||
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@babel/helper-globals@8.0.0-rc.6':
|
||||||
resolution: {integrity: sha512-ZF5FsxE4y7Eb6DiEbsLW3Gz3e73U/Uq3/ZotWf/moxv0DnZziXSMnW/tcXJF8b1mQE8XJnwPNSQOksKcKTz8kQ==}
|
resolution: {integrity: sha512-ZF5FsxE4y7Eb6DiEbsLW3Gz3e73U/Uq3/ZotWf/moxv0DnZziXSMnW/tcXJF8b1mQE8XJnwPNSQOksKcKTz8kQ==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
@ -217,6 +232,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@babel/helper-string-parser@8.0.0-rc.6':
|
||||||
resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==}
|
resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
@ -229,6 +248,10 @@ packages:
|
||||||
resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==}
|
resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
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':
|
'@babel/helper-validator-option@7.29.7':
|
||||||
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
|
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
@ -255,6 +278,11 @@ packages:
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
hasBin: true
|
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':
|
'@babel/plugin-syntax-jsx@7.29.7':
|
||||||
resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==}
|
resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
@ -289,6 +317,10 @@ packages:
|
||||||
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@babel/template@8.0.0-rc.6':
|
||||||
resolution: {integrity: sha512-CZFjZblLiIUEFghbB3sKs3rpYrwo65rLIklw/gNpwBm326cU6TH/xzvJZlZ+H9HpDi00eDXqNJR8CtEKW3oc/A==}
|
resolution: {integrity: sha512-CZFjZblLiIUEFghbB3sKs3rpYrwo65rLIklw/gNpwBm326cU6TH/xzvJZlZ+H9HpDi00eDXqNJR8CtEKW3oc/A==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
@ -301,6 +333,10 @@ packages:
|
||||||
resolution: {integrity: sha512-/txuBViGaE9gpM8MSGShru1O2Bzp3zb3m/XKZNUsNv2SyNn++lKDDIWkiIQ/345j2FBy7CHiYTZuKhewHWyefw==}
|
resolution: {integrity: sha512-/txuBViGaE9gpM8MSGShru1O2Bzp3zb3m/XKZNUsNv2SyNn++lKDDIWkiIQ/345j2FBy7CHiYTZuKhewHWyefw==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
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':
|
'@babel/types@7.29.7':
|
||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
@ -309,6 +345,10 @@ packages:
|
||||||
resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==}
|
resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==}
|
||||||
engines: {node: ^22.18.0 || >=24.11.0}
|
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':
|
'@biomejs/biome@2.4.16':
|
||||||
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
|
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
|
|
@ -559,6 +599,10 @@ packages:
|
||||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
engines: {node: '>=18.0.0'}
|
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':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
|
@ -610,6 +654,9 @@ packages:
|
||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^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':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -1048,6 +1095,9 @@ packages:
|
||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
anynum@1.0.1:
|
||||||
|
resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==}
|
||||||
|
|
||||||
app-builder-bin@5.0.0-alpha.12:
|
app-builder-bin@5.0.0-alpha.12:
|
||||||
resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==}
|
resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==}
|
||||||
|
|
||||||
|
|
@ -1408,8 +1458,8 @@ packages:
|
||||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
electron@43.1.1:
|
electron@43.2.0:
|
||||||
resolution: {integrity: sha512-I5c5vfuVvaXpWx3IZdwvXgxQW44+e7OP1wXGVQkogLeSFSkUZ6sLCcWV05AdEcs65AO5tAIJJwbp7ixw+LdarA==}
|
resolution: {integrity: sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==}
|
||||||
engines: {node: '>= 22.12.0'}
|
engines: {node: '>= 22.12.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
|
@ -1506,6 +1556,13 @@ packages:
|
||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
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:
|
fastq@1.20.1:
|
||||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||||
|
|
||||||
|
|
@ -1771,6 +1828,9 @@ packages:
|
||||||
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
|
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-unsafe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==}
|
||||||
|
|
||||||
isbinaryfile@4.0.10:
|
isbinaryfile@4.0.10:
|
||||||
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
|
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
|
|
@ -2125,6 +2185,10 @@ packages:
|
||||||
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
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:
|
path-is-absolute@1.0.1:
|
||||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -2446,6 +2510,9 @@ packages:
|
||||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
strnum@2.4.1:
|
||||||
|
resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==}
|
||||||
|
|
||||||
sumchecker@3.0.1:
|
sumchecker@3.0.1:
|
||||||
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
|
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
|
||||||
engines: {node: '>= 8.0'}
|
engines: {node: '>= 8.0'}
|
||||||
|
|
@ -2610,6 +2677,10 @@ packages:
|
||||||
resolution: {integrity: sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==}
|
resolution: {integrity: sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==}
|
||||||
engines: {node: '>= 16'}
|
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:
|
xml-parser-xo@4.1.6:
|
||||||
resolution: {integrity: sha512-mXbSNSM+rIwndoxSwmFa83bOdeP8G/cOGr7XvxA67X7ebCzoAuVez9JYDCDub3zRDNBZxIbfjuXLSsamr7NfwQ==}
|
resolution: {integrity: sha512-mXbSNSM+rIwndoxSwmFa83bOdeP8G/cOGr7XvxA67X7ebCzoAuVez9JYDCDub3zRDNBZxIbfjuXLSsamr7NfwQ==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
|
|
@ -2657,6 +2728,11 @@ snapshots:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
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':
|
'@babel/code-frame@8.0.0-rc.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 8.0.0-rc.6
|
'@babel/helper-validator-identifier': 8.0.0-rc.6
|
||||||
|
|
@ -2688,14 +2764,14 @@ snapshots:
|
||||||
|
|
||||||
'@babel/core@8.0.0-rc.6':
|
'@babel/core@8.0.0-rc.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 8.0.0-rc.6
|
'@babel/code-frame': 8.0.0
|
||||||
'@babel/generator': 8.0.0-rc.6
|
'@babel/generator': 8.0.0
|
||||||
'@babel/helper-compilation-targets': 8.0.0-rc.6
|
'@babel/helper-compilation-targets': 8.0.0-rc.6
|
||||||
'@babel/helpers': 8.0.0-rc.6
|
'@babel/helpers': 8.0.0-rc.6
|
||||||
'@babel/parser': 8.0.0-rc.6
|
'@babel/parser': 8.0.4
|
||||||
'@babel/template': 8.0.0-rc.6
|
'@babel/template': 8.0.0
|
||||||
'@babel/traverse': 8.0.0-rc.6
|
'@babel/traverse': 8.0.4
|
||||||
'@babel/types': 8.0.0-rc.6
|
'@babel/types': 8.0.4
|
||||||
'@types/gensync': 1.0.5
|
'@types/gensync': 1.0.5
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
find-up-simple: 1.0.1
|
find-up-simple: 1.0.1
|
||||||
|
|
@ -2713,10 +2789,19 @@ snapshots:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jsesc: 3.1.0
|
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':
|
'@babel/generator@8.0.0-rc.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 8.0.0-rc.6
|
'@babel/parser': 8.0.0-rc.6
|
||||||
'@babel/types': 8.0.0-rc.6
|
'@babel/types': 8.0.4
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
'@types/jsesc': 2.5.1
|
'@types/jsesc': 2.5.1
|
||||||
|
|
@ -2757,6 +2842,8 @@ snapshots:
|
||||||
|
|
||||||
'@babel/helper-globals@7.29.7': {}
|
'@babel/helper-globals@7.29.7': {}
|
||||||
|
|
||||||
|
'@babel/helper-globals@8.0.0': {}
|
||||||
|
|
||||||
'@babel/helper-globals@8.0.0-rc.6': {}
|
'@babel/helper-globals@8.0.0-rc.6': {}
|
||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.29.7':
|
'@babel/helper-member-expression-to-functions@7.29.7':
|
||||||
|
|
@ -2819,12 +2906,16 @@ snapshots:
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7': {}
|
'@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-string-parser@8.0.0-rc.6': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7': {}
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@8.0.0-rc.6': {}
|
'@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@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-option@8.0.0-rc.6': {}
|
'@babel/helper-validator-option@8.0.0-rc.6': {}
|
||||||
|
|
@ -2836,8 +2927,8 @@ snapshots:
|
||||||
|
|
||||||
'@babel/helpers@8.0.0-rc.6':
|
'@babel/helpers@8.0.0-rc.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/template': 8.0.0-rc.6
|
'@babel/template': 8.0.0
|
||||||
'@babel/types': 8.0.0-rc.6
|
'@babel/types': 8.0.4
|
||||||
|
|
||||||
'@babel/parser@7.29.7':
|
'@babel/parser@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -2845,18 +2936,17 @@ snapshots:
|
||||||
|
|
||||||
'@babel/parser@8.0.0-rc.6':
|
'@babel/parser@8.0.0-rc.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 8.0.0-rc.6
|
'@babel/types': 8.0.4
|
||||||
|
|
||||||
|
'@babel/parser@8.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 8.0.4
|
||||||
|
|
||||||
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
|
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-plugin-utils': 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)':
|
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
|
|
@ -2898,6 +2988,12 @@ snapshots:
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 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':
|
'@babel/template@8.0.0-rc.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 8.0.0-rc.6
|
'@babel/code-frame': 8.0.0-rc.6
|
||||||
|
|
@ -2926,6 +3022,16 @@ snapshots:
|
||||||
'@babel/types': 8.0.0-rc.6
|
'@babel/types': 8.0.0-rc.6
|
||||||
obug: 2.1.1
|
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':
|
'@babel/types@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.29.7
|
'@babel/helper-string-parser': 7.29.7
|
||||||
|
|
@ -2936,6 +3042,11 @@ snapshots:
|
||||||
'@babel/helper-string-parser': 8.0.0-rc.6
|
'@babel/helper-string-parser': 8.0.0-rc.6
|
||||||
'@babel/helper-validator-identifier': 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':
|
'@biomejs/biome@2.4.16':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@biomejs/cli-darwin-arm64': 2.4.16
|
'@biomejs/cli-darwin-arm64': 2.4.16
|
||||||
|
|
@ -3156,6 +3267,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.3
|
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':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
@ -3237,6 +3353,8 @@ snapshots:
|
||||||
'@tybys/wasm-util': 0.10.2
|
'@tybys/wasm-util': 0.10.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nodable/entities@3.0.0': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
|
|
@ -3599,6 +3717,8 @@ snapshots:
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
picomatch: 2.3.2
|
picomatch: 2.3.2
|
||||||
|
|
||||||
|
anynum@1.0.1: {}
|
||||||
|
|
||||||
app-builder-bin@5.0.0-alpha.12: {}
|
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):
|
app-builder-lib@26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1):
|
||||||
|
|
@ -3690,19 +3810,19 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@8.0.0-rc.6):
|
babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 8.0.0-rc.6
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-module-imports': 7.18.6
|
'@babel/helper-module-imports': 7.18.6
|
||||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@8.0.0-rc.6)
|
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
html-entities: 2.3.3
|
html-entities: 2.3.3
|
||||||
parse5: 7.3.0
|
parse5: 7.3.0
|
||||||
|
|
||||||
babel-preset-solid@1.9.12(@babel/core@8.0.0-rc.6)(solid-js@1.9.14):
|
babel-preset-solid@1.9.12(@babel/core@7.29.7)(solid-js@1.9.14):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 8.0.0-rc.6
|
'@babel/core': 7.29.7
|
||||||
babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@8.0.0-rc.6)
|
babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.7)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
solid-js: 1.9.14
|
solid-js: 1.9.14
|
||||||
|
|
||||||
|
|
@ -4088,7 +4208,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
electron@43.1.1:
|
electron@43.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electron-internal/extract-zip': 1.0.4
|
'@electron-internal/extract-zip': 1.0.4
|
||||||
'@electron/get': 5.0.0
|
'@electron/get': 5.0.0
|
||||||
|
|
@ -4192,6 +4312,20 @@ snapshots:
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
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:
|
fastq@1.20.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.1.0
|
reusify: 1.1.0
|
||||||
|
|
@ -4494,6 +4628,8 @@ snapshots:
|
||||||
|
|
||||||
is-plain-object@3.0.1: {}
|
is-plain-object@3.0.1: {}
|
||||||
|
|
||||||
|
is-unsafe@2.0.0: {}
|
||||||
|
|
||||||
isbinaryfile@4.0.10: {}
|
isbinaryfile@4.0.10: {}
|
||||||
|
|
||||||
isbinaryfile@5.0.7: {}
|
isbinaryfile@5.0.7: {}
|
||||||
|
|
@ -4789,6 +4925,8 @@ snapshots:
|
||||||
|
|
||||||
path-exists@5.0.0: {}
|
path-exists@5.0.0: {}
|
||||||
|
|
||||||
|
path-expression-matcher@1.6.2: {}
|
||||||
|
|
||||||
path-is-absolute@1.0.1: {}
|
path-is-absolute@1.0.1: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
@ -5129,6 +5267,10 @@ snapshots:
|
||||||
strip-json-comments@2.0.1:
|
strip-json-comments@2.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
strnum@2.4.1:
|
||||||
|
dependencies:
|
||||||
|
anynum: 1.0.1
|
||||||
|
|
||||||
sumchecker@3.0.1:
|
sumchecker@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
|
|
@ -5282,6 +5424,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
xml-parser-xo: 4.1.6
|
xml-parser-xo: 4.1.6
|
||||||
|
|
||||||
|
xml-naming@0.3.0: {}
|
||||||
|
|
||||||
xml-parser-xo@4.1.6: {}
|
xml-parser-xo@4.1.6: {}
|
||||||
|
|
||||||
xmlbuilder@15.1.1: {}
|
xmlbuilder@15.1.1: {}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ allowBuilds:
|
||||||
koffi: true
|
koffi: true
|
||||||
|
|
||||||
minimumReleaseAgeExclude:
|
minimumReleaseAgeExclude:
|
||||||
- electron@42.3.3
|
- electron@42.3.3 || 43.2.0
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
yauzl: "^3.3.1" # Electron still can't get their shit together https://github.com/electron/electron/issues/51619
|
yauzl: "^3.3.1" # Electron still can't get their shit together https://github.com/electron/electron/issues/51619
|
||||||
|
|
|
||||||
22
src/@types/settings.d.ts
vendored
22
src/@types/settings.d.ts
vendored
|
|
@ -25,6 +25,9 @@ export interface AudioSettings {
|
||||||
loopbackType: "loopback" | "loopbackWithMute";
|
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 {
|
export interface Settings {
|
||||||
// Referenced for detecting a broken config.
|
// Referenced for detecting a broken config.
|
||||||
"0"?: string;
|
"0"?: string;
|
||||||
|
|
@ -43,7 +46,16 @@ export interface Settings {
|
||||||
mods: ValidMods[];
|
mods: ValidMods[];
|
||||||
mobileMode: boolean;
|
mobileMode: boolean;
|
||||||
skipSplash: boolean;
|
skipSplash: boolean;
|
||||||
performanceMode: "battery" | "dynamic" | "performance" | "smoothScreenshare" | "none";
|
performanceMode:
|
||||||
|
| "battery"
|
||||||
|
| "dynamic"
|
||||||
|
| "performance"
|
||||||
|
| "balanced"
|
||||||
|
| "memory"
|
||||||
|
| "voip"
|
||||||
|
| "latency"
|
||||||
|
| "smoothScreenshare"
|
||||||
|
| "none";
|
||||||
customJsBundle: RequestInfo | URL | string;
|
customJsBundle: RequestInfo | URL | string;
|
||||||
customCssBundle: RequestInfo | URL | string;
|
customCssBundle: RequestInfo | URL | string;
|
||||||
startMinimized: boolean;
|
startMinimized: boolean;
|
||||||
|
|
@ -67,6 +79,14 @@ export interface Settings {
|
||||||
quickCss: boolean;
|
quickCss: boolean;
|
||||||
autoScroll: boolean;
|
autoScroll: boolean;
|
||||||
additionalArguments: string;
|
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[];
|
noBundleUpdates: ValidMods[];
|
||||||
automaticUpdates: boolean;
|
automaticUpdates: boolean;
|
||||||
overlayButtonColor: string;
|
overlayButtonColor: string;
|
||||||
|
|
|
||||||
28
src/common/commandDefinitions.ts
Normal file
28
src/common/commandDefinitions.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// 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];
|
||||||
|
|
@ -60,6 +60,10 @@ const defaults: Settings = {
|
||||||
noBundleUpdates: [],
|
noBundleUpdates: [],
|
||||||
automaticUpdates: false,
|
automaticUpdates: false,
|
||||||
additionalArguments: "",
|
additionalArguments: "",
|
||||||
|
proxyMode: "system",
|
||||||
|
proxyRules: "",
|
||||||
|
proxyBypassRules: "<local>",
|
||||||
|
proxyPacScript: "",
|
||||||
customIcon: join(import.meta.dirname, "../", "/assets/desktop.png"),
|
customIcon: join(import.meta.dirname, "../", "/assets/desktop.png"),
|
||||||
smoothScroll: true,
|
smoothScroll: true,
|
||||||
autoScroll: false,
|
autoScroll: false,
|
||||||
|
|
|
||||||
|
|
@ -47,5 +47,6 @@ export function navigateTo(passedWindow: BrowserWindow, url: string): void {
|
||||||
passedWindow.webContents.executeJavaScript(
|
passedWindow.webContents.executeJavaScript(
|
||||||
`history.pushState({}, null, ${JSON.stringify(sanitized)});window.dispatchEvent(new PopStateEvent("popstate", {}));`,
|
`history.pushState({}, null, ${JSON.stringify(sanitized)});window.dispatchEvent(new PopStateEvent("popstate", {}));`,
|
||||||
);
|
);
|
||||||
|
passedWindow.show();
|
||||||
passedWindow.focus();
|
passedWindow.focus();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,76 @@ const performance: Preset = {
|
||||||
disableFeatures: ["Vulkan"],
|
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 = {
|
const smoothExperiment: Preset = {
|
||||||
switches: [
|
switches: [
|
||||||
["enable-gpu-rasterization"],
|
["enable-gpu-rasterization"],
|
||||||
|
|
@ -54,6 +124,7 @@ const smoothExperiment: Preset = {
|
||||||
"AcceleratedVideoEncoder",
|
"AcceleratedVideoEncoder",
|
||||||
"AcceleratedVideoDecoder",
|
"AcceleratedVideoDecoder",
|
||||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
||||||
|
"ZeroCopyDesktopCapture",
|
||||||
],
|
],
|
||||||
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
|
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +137,7 @@ const battery: Preset = {
|
||||||
["enable-low-res-tiling"],
|
["enable-low-res-tiling"],
|
||||||
["process-per-site"],
|
["process-per-site"],
|
||||||
],
|
],
|
||||||
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery"],
|
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery", "CalculateNativeWinOcclusion"],
|
||||||
disableFeatures: [],
|
disableFeatures: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -160,6 +231,14 @@ 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 {
|
export function getPreset(): Preset | undefined {
|
||||||
// MIT License
|
// MIT License
|
||||||
|
|
||||||
|
|
@ -182,31 +261,58 @@ export function getPreset(): Preset | undefined {
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
// 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
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
if (getConfig("vaapi")) {
|
let preset: Preset | undefined;
|
||||||
console.log("VAAPI mode enabled");
|
|
||||||
mergeWithCustomFlags(vaapi);
|
|
||||||
}
|
|
||||||
switch (getConfig("performanceMode")) {
|
switch (getConfig("performanceMode")) {
|
||||||
case "dynamic":
|
case "dynamic":
|
||||||
if (powerMonitor.isOnBatteryPower()) {
|
if (powerMonitor.isOnBatteryPower()) {
|
||||||
console.log("Battery mode enabled");
|
console.log("Battery mode enabled");
|
||||||
return mergeWithCustomFlags(battery);
|
preset = battery;
|
||||||
} else {
|
} else {
|
||||||
console.log("Performance mode enabled");
|
console.log("Performance mode enabled");
|
||||||
return mergeWithCustomFlags(performance);
|
preset = performance;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
case "performance":
|
case "performance":
|
||||||
console.log("Performance mode enabled");
|
console.log("Performance mode enabled");
|
||||||
return mergeWithCustomFlags(performance);
|
preset = performance;
|
||||||
|
break;
|
||||||
|
case "balanced":
|
||||||
|
console.log("Balanced mode enabled");
|
||||||
|
preset = balanced;
|
||||||
|
break;
|
||||||
case "battery":
|
case "battery":
|
||||||
console.log("Battery mode enabled");
|
console.log("Battery mode enabled");
|
||||||
return mergeWithCustomFlags(battery);
|
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;
|
||||||
case "smoothScreenshare":
|
case "smoothScreenshare":
|
||||||
console.log("Smooth screenshare mode enabled");
|
console.log("Smooth screenshare mode enabled");
|
||||||
return mergeWithCustomFlags(smoothExperiment);
|
preset = smoothExperiment;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("No performance modes set");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
86
src/common/handleCommands.ts
Normal file
86
src/common/handleCommands.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
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("");
|
||||||
|
}
|
||||||
201
src/common/proxy.ts
Normal file
201
src/common/proxy.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/common/windowBounds.ts
Normal file
169
src/common/windowBounds.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
158
src/dbus.ts
Normal file
158
src/dbus.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
75
src/discord/preload/inviteBackButton.mts
Normal file
75
src/discord/preload/inviteBackButton.mts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
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);
|
||||||
|
})();
|
||||||
|
`);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
@ -3,15 +3,22 @@ const { ipcRenderer } = require("electron");
|
||||||
import { addStyle } from "../../common/dom.js";
|
import { addStyle } from "../../common/dom.js";
|
||||||
import { sleep } from "../../common/sleep.js";
|
import { sleep } from "../../common/sleep.js";
|
||||||
|
|
||||||
if (
|
const windowStyle = ipcRenderer.sendSync("getConfig", "windowStyle") as string;
|
||||||
ipcRenderer.sendSync("getConfig", "windowStyle") === "default" ||
|
const transparency = ipcRenderer.sendSync("getConfig", "transparency") as string;
|
||||||
ipcRenderer.sendSync("getConfig", "windowStyle") === "overlay"
|
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) {
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.body.setAttribute("legcord-platform", ipcRenderer.sendSync("getOS"));
|
document.body.setAttribute("legcord-platform", os);
|
||||||
addStyle("legcord://assets/css/baseTitlebar.css");
|
addStyle("legcord://assets/css/baseTitlebar.css");
|
||||||
sleep(500);
|
sleep(500);
|
||||||
switch (ipcRenderer.sendSync("getOS")) {
|
switch (os) {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
// breaks traffic lights with bar__ and hidden__ classes
|
// breaks traffic lights with bar__ and hidden__ classes
|
||||||
// document.body.setAttribute("class", "platform-osx");
|
// document.body.setAttribute("class", "platform-osx");
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import "./patches.mjs";
|
||||||
import "./newTitlebar.mjs";
|
import "./newTitlebar.mjs";
|
||||||
import "./titlebar.mjs";
|
import "./titlebar.mjs";
|
||||||
import "./themes.js";
|
import "./themes.js";
|
||||||
|
import "./inviteBackButton.mjs";
|
||||||
|
|
||||||
console.log("Legcord");
|
console.log("Legcord");
|
||||||
window.localStorage.setItem("hideNag", "true");
|
window.localStorage.setItem("hideNag", "true");
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,49 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Worker } from "node:worker_threads";
|
import { Worker } from "node:worker_threads";
|
||||||
import type { GameList } from "arrpc";
|
import type { GameList, ServerSettings } from "arrpc";
|
||||||
import type { BrowserWindow } from "electron";
|
import type { BrowserWindow } from "electron";
|
||||||
|
import { getConfig } from "../common/config.js";
|
||||||
import { getDetectables } from "../common/detectables.js";
|
import { getDetectables } from "../common/detectables.js";
|
||||||
import { navigateTo } from "../common/dom.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;
|
let rpcWorker: Worker;
|
||||||
export let processList: GameList[] = [];
|
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) {
|
export function startRPC(window: BrowserWindow) {
|
||||||
if (rpcWorker) {
|
if (rpcWorker) {
|
||||||
rpcWorker.terminate();
|
rpcWorker.terminate();
|
||||||
}
|
}
|
||||||
const rpcPath = path.join(__dirname, "rpc.js");
|
const rpcPath = path.join(__dirname, "rpc.js");
|
||||||
|
const settings = getRpcServerSettings();
|
||||||
|
|
||||||
rpcWorker = new Worker(rpcPath, {
|
rpcWorker = new Worker(rpcPath, {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
detectables: JSON.stringify(getDetectables()),
|
detectables: JSON.stringify(getDetectables()),
|
||||||
|
settings: JSON.stringify(settings),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
rpcWorker.on("online", () => {
|
rpcWorker.on("online", () => {
|
||||||
console.log("[arRPC] process started");
|
console.log("[arRPC] process started", settings);
|
||||||
console.log(rpcWorker.threadId);
|
console.log(rpcWorker.threadId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,18 @@ import contextMenu from "electron-context-menu";
|
||||||
import { firstRun, getConfig, setConfig } from "../common/config.js";
|
import { firstRun, getConfig, setConfig } from "../common/config.js";
|
||||||
import { navigateTo } from "../common/dom.js";
|
import { navigateTo } from "../common/dom.js";
|
||||||
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
|
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
|
||||||
|
import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
|
||||||
import { getLang } from "../common/lang.js";
|
import { getLang } from "../common/lang.js";
|
||||||
import { injectThemesMain } from "../common/themes.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 { getWindowState, setWindowState } from "../common/windowState.js";
|
||||||
|
import { disconnectDbusService } from "../dbus.js";
|
||||||
import { init } from "../main.js";
|
import { init } from "../main.js";
|
||||||
import { registerGlobalKeybinds } from "./globalKeybinds.js";
|
import { registerGlobalKeybinds } from "./globalKeybinds.js";
|
||||||
import { registerIpc } from "./ipc.js";
|
import { registerIpc } from "./ipc.js";
|
||||||
|
|
@ -32,27 +41,17 @@ import { registerVenmicIpc } from "./venmic.js";
|
||||||
export let mainWindows: BrowserWindow[] = [];
|
export let mainWindows: BrowserWindow[] = [];
|
||||||
export let inviteWindow: BrowserWindow;
|
export let inviteWindow: BrowserWindow;
|
||||||
|
|
||||||
function getStoredWindowBounds(): BrowserWindowConstructorOptions {
|
function getStoredWindowBounds() {
|
||||||
const width = getWindowState("width") ?? 835;
|
return sanitizeWindowBounds(
|
||||||
const height = getWindowState("height") ?? 600;
|
{
|
||||||
const x = getWindowState("x");
|
width: getWindowState("width"),
|
||||||
const y = getWindowState("y");
|
height: getWindowState("height"),
|
||||||
|
x: getWindowState("x"),
|
||||||
if (x === undefined || y === undefined) {
|
y: getWindowState("y"),
|
||||||
return {
|
displayId: getWindowState("displayId"),
|
||||||
width,
|
},
|
||||||
height,
|
screen.getAllDisplays(),
|
||||||
};
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
// Save window bounds using the same API family we restore with.
|
||||||
|
|
@ -61,16 +60,25 @@ function saveWindowState(win: BrowserWindow): void {
|
||||||
try {
|
try {
|
||||||
const [x, y] = win.getPosition();
|
const [x, y] = win.getPosition();
|
||||||
const [width, height] = win.getSize();
|
const [width, height] = win.getSize();
|
||||||
const display = screen.getDisplayNearestPoint({ x, y });
|
const sanitized = sanitizeWindowBounds(
|
||||||
|
{
|
||||||
setWindowState({
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
isMaximized: win.isMaximized(),
|
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
displayId: display.id,
|
displayId: screen.getDisplayNearestPoint({ x, y }).id,
|
||||||
displayScaleFactor: display.scaleFactor,
|
},
|
||||||
|
screen.getAllDisplays(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setWindowState({
|
||||||
|
width: sanitized.width,
|
||||||
|
height: sanitized.height,
|
||||||
|
isMaximized: win.isMaximized(),
|
||||||
|
x: sanitized.x,
|
||||||
|
y: sanitized.y,
|
||||||
|
displayId: sanitized.displayId,
|
||||||
|
displayScaleFactor: sanitized.displayScaleFactor,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("[Window] Failed to save window state:", e);
|
console.log("[Window] Failed to save window state:", e);
|
||||||
|
|
@ -135,7 +143,7 @@ contextMenu({
|
||||||
function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
createTray();
|
createTray();
|
||||||
if (getWindowState("isMaximized") ?? false) {
|
if (getWindowState("isMaximized") ?? false) {
|
||||||
passedWindow.setSize(835, 600); //just so the whole thing doesn't cover whole screen
|
passedWindow.setSize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT); //just so the whole thing doesn't cover whole screen
|
||||||
passedWindow.maximize();
|
passedWindow.maximize();
|
||||||
void passedWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
|
void passedWindow.webContents.executeJavaScript(`document.body.setAttribute("isMaximized", "");`);
|
||||||
passedWindow.hide(); // please don't flashbang the user
|
passedWindow.hide(); // please don't flashbang the user
|
||||||
|
|
@ -160,17 +168,18 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
app.on("second-instance", (_event, commandLine, _workingDirectory, additionalData) => {
|
app.on("second-instance", (_event, commandLine, _workingDirectory, additionalData) => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
// Print out data received from the second instance.
|
// Print out data received from the second instance.
|
||||||
console.log(additionalData);
|
console.log(`data received: ${additionalData}`);
|
||||||
|
|
||||||
if (!getConfig("multiInstance")) {
|
if (!getConfig("multiInstance")) {
|
||||||
// Someone tried to run a second instance, we should focus our window.
|
// Someone tried to run a second instance,
|
||||||
if (passedWindow) {
|
// we should focus our window if the user is not running special commands.
|
||||||
|
if (passedWindow && !passedValidArgument(commandLine)) {
|
||||||
if (passedWindow.isMinimized()) passedWindow.restore();
|
if (passedWindow.isMinimized()) passedWindow.restore();
|
||||||
passedWindow.show();
|
passedWindow.show();
|
||||||
passedWindow.focus();
|
passedWindow.focus();
|
||||||
}
|
}
|
||||||
if (commandLine && commandLine.length > 0) {
|
if (commandLine && commandLine.length > 0) {
|
||||||
console.log(commandLine);
|
handleCommands(commandLine);
|
||||||
const lastArg = commandLine.pop();
|
const lastArg = commandLine.pop();
|
||||||
if (lastArg?.startsWith("discord://-")) {
|
if (lastArg?.startsWith("discord://-")) {
|
||||||
navigateTo(passedWindow, lastArg.replace("discord://-", ""));
|
navigateTo(passedWindow, lastArg.replace("discord://-", ""));
|
||||||
|
|
@ -383,6 +392,7 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
});
|
});
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
stopRPC();
|
stopRPC();
|
||||||
|
disconnectDbusService();
|
||||||
try {
|
try {
|
||||||
// Ensure current window state is saved with display info
|
// Ensure current window state is saved with display info
|
||||||
if (passedWindow && !passedWindow.isDestroyed()) saveWindowState(passedWindow);
|
if (passedWindow && !passedWindow.isDestroyed()) saveWindowState(passedWindow);
|
||||||
|
|
@ -484,10 +494,15 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
||||||
|
|
||||||
export function createWindow() {
|
export function createWindow() {
|
||||||
const storedBounds = getStoredWindowBounds();
|
const storedBounds = getStoredWindowBounds();
|
||||||
|
if (storedBounds.usedFallback) {
|
||||||
|
console.log("[Window] Stored bounds were invalid or off-screen; using sanitized placement", storedBounds);
|
||||||
|
}
|
||||||
const browserWindowOptions: BrowserWindowConstructorOptions = {
|
const browserWindowOptions: BrowserWindowConstructorOptions = {
|
||||||
// Use safe defaults for constructor; actual bounds applied via setBounds() below
|
// Use safe defaults for constructor; actual bounds applied via setPosition/setSize below
|
||||||
width: 835,
|
width: DEFAULT_WINDOW_WIDTH,
|
||||||
height: 600,
|
height: DEFAULT_WINDOW_HEIGHT,
|
||||||
|
minWidth: MIN_WINDOW_WIDTH,
|
||||||
|
minHeight: MIN_WINDOW_HEIGHT,
|
||||||
title: "Legcord",
|
title: "Legcord",
|
||||||
show: false,
|
show: false,
|
||||||
darkTheme: true,
|
darkTheme: true,
|
||||||
|
|
@ -512,7 +527,22 @@ export function createWindow() {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "native":
|
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;
|
break;
|
||||||
case "overlay":
|
case "overlay":
|
||||||
browserWindowOptions.titleBarStyle = "hidden";
|
browserWindowOptions.titleBarStyle = "hidden";
|
||||||
|
|
@ -550,15 +580,8 @@ export function createWindow() {
|
||||||
const mainWindow = new BrowserWindow(browserWindowOptions);
|
const mainWindow = new BrowserWindow(browserWindowOptions);
|
||||||
|
|
||||||
// Restore by position + size directly to match saveWindowState roundtrip.
|
// Restore by position + size directly to match saveWindowState roundtrip.
|
||||||
if (
|
|
||||||
storedBounds.x !== undefined &&
|
|
||||||
storedBounds.y !== undefined &&
|
|
||||||
storedBounds.width !== undefined &&
|
|
||||||
storedBounds.height !== undefined
|
|
||||||
) {
|
|
||||||
mainWindow.setPosition(storedBounds.x, storedBounds.y);
|
mainWindow.setPosition(storedBounds.x, storedBounds.y);
|
||||||
mainWindow.setSize(storedBounds.width, storedBounds.height);
|
mainWindow.setSize(storedBounds.width, storedBounds.height);
|
||||||
}
|
|
||||||
|
|
||||||
mainWindows.push(mainWindow);
|
mainWindows.push(mainWindow);
|
||||||
doAfterDefiningTheWindow(mainWindow);
|
doAfterDefiningTheWindow(mainWindow);
|
||||||
|
|
|
||||||
17
src/main.ts
17
src/main.ts
|
|
@ -18,6 +18,8 @@ import {
|
||||||
} from "./common/config.js";
|
} from "./common/config.js";
|
||||||
import { getPreset } from "./common/flags.js";
|
import { getPreset } from "./common/flags.js";
|
||||||
import { setLang } from "./common/lang.js";
|
import { setLang } from "./common/lang.js";
|
||||||
|
import { applyProxyCommandLineSwitches, applySessionProxy, configureNodeProxyEnv } from "./common/proxy.js";
|
||||||
|
import { setupGlobalShortcuts, startDbusService } from "./dbus.js";
|
||||||
|
|
||||||
// Chrome flags tracking
|
// Chrome flags tracking
|
||||||
export interface AppliedFlagsOutput {
|
export interface AppliedFlagsOutput {
|
||||||
|
|
@ -153,7 +155,7 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
|
||||||
.add("MediaSessionService");
|
.add("MediaSessionService");
|
||||||
// Your data now belongs to CCP
|
// Your data now belongs to CCP
|
||||||
crashReporter.start({ uploadToServer: false });
|
crashReporter.start({ uploadToServer: false });
|
||||||
// enable pulseaudio audio sharing on linux
|
// enable pulseaudio audio sharing on linux and register keybinds on supported desktop managers and start dbus socket
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
app.commandLine.appendSwitch("gtk-version", "3");
|
app.commandLine.appendSwitch("gtk-version", "3");
|
||||||
trackSwitch("gtk-version", "3");
|
trackSwitch("gtk-version", "3");
|
||||||
|
|
@ -161,6 +163,16 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
|
||||||
disableFeatures.add("WebRtcAllowInputVolumeAdjustment");
|
disableFeatures.add("WebRtcAllowInputVolumeAdjustment");
|
||||||
app.commandLine.appendSwitch("enable-speech-dispatcher");
|
app.commandLine.appendSwitch("enable-speech-dispatcher");
|
||||||
trackSwitch("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
|
// enable webrtc capturer for wayland
|
||||||
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") {
|
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") {
|
||||||
|
|
@ -182,6 +194,8 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
|
||||||
app.commandLine.appendSwitch("enable-transparent-visuals");
|
app.commandLine.appendSwitch("enable-transparent-visuals");
|
||||||
trackSwitch("enable-transparent-visuals");
|
trackSwitch("enable-transparent-visuals");
|
||||||
checkIfConfigIsBroken();
|
checkIfConfigIsBroken();
|
||||||
|
configureNodeProxyEnv();
|
||||||
|
applyProxyCommandLineSwitches();
|
||||||
const preset = getPreset();
|
const preset = getPreset();
|
||||||
if (preset) {
|
if (preset) {
|
||||||
preset.switches.forEach(([key, val]) => {
|
preset.switches.forEach(([key, val]) => {
|
||||||
|
|
@ -322,6 +336,7 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
|
||||||
|
|
||||||
void app.whenReady().then(async () => {
|
void app.whenReady().then(async () => {
|
||||||
if (isDev) console.log(JSON.stringify(getAppliedFlags()));
|
if (isDev) console.log(JSON.stringify(getAppliedFlags()));
|
||||||
|
await applySessionProxy();
|
||||||
process.on("SIGINT", () => app.quit());
|
process.on("SIGINT", () => app.quit());
|
||||||
process.on("SIGTERM", () => 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)
|
// Patch for linux bug to ensure things are loaded before window creation (fixes transparency on some linux systems)
|
||||||
|
|
|
||||||
11
src/rpc.ts
11
src/rpc.ts
|
|
@ -5,9 +5,16 @@ import { parentPort } from "node:worker_threads";
|
||||||
import RPCServer, { type GameList, type ServerSettings } from "arrpc";
|
import RPCServer, { type GameList, type ServerSettings } from "arrpc";
|
||||||
|
|
||||||
const detectables: GameList = process.env.detectables ? JSON.parse(process.env.detectables) : [];
|
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
|
const settings: ServerSettings = process.env.settings
|
||||||
? JSON.parse(process.env.settings)
|
? { ...defaultSettings, ...JSON.parse(process.env.settings) }
|
||||||
: { processScanning: true, windowsLegacyScanning: false, scanInterval: 5000 };
|
: defaultSettings;
|
||||||
|
|
||||||
|
console.log("[arRPC] worker settings", settings);
|
||||||
|
|
||||||
const RPC = await new RPCServer(detectables, settings);
|
const RPC = await new RPCServer(detectables, settings);
|
||||||
// Guard parentPort
|
// Guard parentPort
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
.fieldNote {
|
||||||
|
display: block;
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
color: var(--text-muted, var(--text-subtle));
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Game } from "arrpc";
|
import type { Game } from "arrpc";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { setRestartRequired } from "../settings.js";
|
import { setRestartRequired } from "../settings.js";
|
||||||
|
import classes from "./AddDetectableModal.module.css";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ui: {
|
ui: {
|
||||||
|
|
@ -16,6 +17,8 @@ const {
|
||||||
SwitchItem,
|
SwitchItem,
|
||||||
showToast,
|
showToast,
|
||||||
openConfirmationModal,
|
openConfirmationModal,
|
||||||
|
Text,
|
||||||
|
TextTags,
|
||||||
},
|
},
|
||||||
plugin: { store },
|
plugin: { store },
|
||||||
} = shelter;
|
} = shelter;
|
||||||
|
|
@ -88,16 +91,25 @@ export const AddDetectableModal = (props: { close: () => void; executable: strin
|
||||||
<ModalRoot size={ModalSizes.SMALL}>
|
<ModalRoot size={ModalSizes.SMALL}>
|
||||||
<ModalHeader close={props.close}>{t["detectable-addApp"]}</ModalHeader>
|
<ModalHeader close={props.close}>{t["detectable-addApp"]}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Header tag={HeaderTags.H5}>{t["detectable-appName"]}</Header>
|
<Header tag={HeaderTags.HeadingSM}>{t["detectable-appName"]}</Header>
|
||||||
<TextBox value={appName()} onInput={setAppName} placeholder={t["detectable-placeholderName"]} />
|
<TextBox value={appName()} onInput={setAppName} placeholder={t["detectable-placeholderName"]} />
|
||||||
<Divider mt mb />
|
<Divider mt mb />
|
||||||
<Header tag={HeaderTags.H5}>{t["detectable-appId"]}</Header>
|
<Header tag={HeaderTags.HeadingSM}>{t["detectable-appId"]}</Header>
|
||||||
|
<Text tag={TextTags.textSM} class={classes.fieldNote}>
|
||||||
|
{t["detectable-appId-note"]}
|
||||||
|
</Text>
|
||||||
<TextBox value={appId()} onInput={setAppId} placeholder={t["detectable-placeholderId"]} />
|
<TextBox value={appId()} onInput={setAppId} placeholder={t["detectable-placeholderId"]} />
|
||||||
<Divider mt mb />
|
<Divider mt mb />
|
||||||
<Header tag={HeaderTags.H5}>{t["detectable-themes"]}</Header>
|
<Header tag={HeaderTags.HeadingSM}>{t["detectable-themes"]}</Header>
|
||||||
|
<Text tag={TextTags.textSM} class={classes.fieldNote}>
|
||||||
|
{t["detectable-themes-note"]}
|
||||||
|
</Text>
|
||||||
<TextBox value={themes()} onInput={setThemes} placeholder={t["detectable-placeholderThemes"]} />
|
<TextBox value={themes()} onInput={setThemes} placeholder={t["detectable-placeholderThemes"]} />
|
||||||
<Divider mt mb />
|
<Divider mt mb />
|
||||||
<Header tag={HeaderTags.H5}>{t["detectable-aliases"]}</Header>
|
<Header tag={HeaderTags.HeadingSM}>{t["detectable-aliases"]}</Header>
|
||||||
|
<Text tag={TextTags.textSM} class={classes.fieldNote}>
|
||||||
|
{t["detectable-aliases-note"]}
|
||||||
|
</Text>
|
||||||
<TextBox value={aliases()} onInput={setAliases} placeholder={t["detectable-placeholderAliases"]} />
|
<TextBox value={aliases()} onInput={setAliases} placeholder={t["detectable-placeholderAliases"]} />
|
||||||
<Divider mt mb />
|
<Divider mt mb />
|
||||||
<SwitchItem hideBorder value={enabled()} onChange={setEnabled}>
|
<SwitchItem hideBorder value={enabled()} onChange={setEnabled}>
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,12 @@
|
||||||
.item {
|
.item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embedded {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded .actions {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
import classes from "./BackupSection.module.css";
|
import classes from "./BackupSection.module.css";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -143,7 +143,7 @@ function applyClientModsFromRestore(raw: unknown): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BackupSection() {
|
export function BackupSection(props: { embedded?: boolean }) {
|
||||||
const t = store.i18n;
|
const t = store.i18n;
|
||||||
|
|
||||||
function buildClientMods(includes: IncludeOptions) {
|
function buildClientMods(includes: IncludeOptions) {
|
||||||
|
|
@ -248,11 +248,13 @@ export function BackupSection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes.item}>
|
<div class={props.embedded ? classes.embedded : classes.item}>
|
||||||
|
<Show when={!props.embedded}>
|
||||||
<Header class={classes.title} tag={HeaderTags.H3}>
|
<Header class={classes.title} tag={HeaderTags.H3}>
|
||||||
{t["backup-pageTitle"]}
|
{t["backup-pageTitle"]}
|
||||||
</Header>
|
</Header>
|
||||||
<div class={classes.note}>{t["backup-pageSubtitle"]}</div>
|
<div class={classes.note}>{t["backup-pageSubtitle"]}</div>
|
||||||
|
</Show>
|
||||||
<div class={classes.actions}>
|
<div class={classes.actions}>
|
||||||
<Button onClick={openBackupModal} size={ButtonSizes.LARGE}>
|
<Button onClick={openBackupModal} size={ButtonSizes.LARGE}>
|
||||||
{t["backup-createBackup"]}
|
{t["backup-createBackup"]}
|
||||||
|
|
@ -261,7 +263,9 @@ export function BackupSection() {
|
||||||
{t["backup-restore"]}
|
{t["backup-restore"]}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={!props.embedded}>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
src/shelter/settings/components/EmptyState.module.css
Normal file
27
src/shelter/settings/components/EmptyState.module.css
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
38
src/shelter/settings/components/EmptyState.tsx
Normal file
38
src/shelter/settings/components/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import classes from "./PluginCard.module.css";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ui: { Header, HeaderTags, Switch, Button, ButtonSizes },
|
ui: { Header, HeaderTags, Switch, Button, ButtonSizes },
|
||||||
|
plugin: { store },
|
||||||
} = shelter;
|
} = shelter;
|
||||||
|
|
||||||
export function PluginCard(props: {
|
export function PluginCard(props: {
|
||||||
|
|
@ -11,6 +12,7 @@ export function PluginCard(props: {
|
||||||
onToggle: (enabled: boolean) => void;
|
onToggle: (enabled: boolean) => void;
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = store.i18n;
|
||||||
const targets = [
|
const targets = [
|
||||||
props.plugin.hasMain ? "main" : null,
|
props.plugin.hasMain ? "main" : null,
|
||||||
props.plugin.hasPreload ? "preload" : null,
|
props.plugin.hasPreload ? "preload" : null,
|
||||||
|
|
@ -23,7 +25,7 @@ export function PluginCard(props: {
|
||||||
<div class={classes.card}>
|
<div class={classes.card}>
|
||||||
<div class={classes.main}>
|
<div class={classes.main}>
|
||||||
<div class={classes.titleRow}>
|
<div class={classes.titleRow}>
|
||||||
<Header tag={HeaderTags.H3} class={classes.title}>
|
<Header tag={HeaderTags.HeadingLG} class={classes.title}>
|
||||||
{props.plugin.name}
|
{props.plugin.name}
|
||||||
</Header>
|
</Header>
|
||||||
<Switch checked={props.plugin.enabled} onChange={props.onToggle} disabled={props.busy} />
|
<Switch checked={props.plugin.enabled} onChange={props.onToggle} disabled={props.busy} />
|
||||||
|
|
@ -33,10 +35,12 @@ export function PluginCard(props: {
|
||||||
{props.plugin.author ? ` • ${props.plugin.author}` : ""}
|
{props.plugin.author ? ` • ${props.plugin.author}` : ""}
|
||||||
</div>
|
</div>
|
||||||
{props.plugin.description ? <div class={classes.description}>{props.plugin.description}</div> : null}
|
{props.plugin.description ? <div class={classes.description}>{props.plugin.description}</div> : null}
|
||||||
<div class={classes.targets}>Targets: {targets || "none"}</div>
|
<div class={classes.targets}>
|
||||||
|
{t["plugins-targets"]}: {targets || t["plugins-targetsNone"]}
|
||||||
|
</div>
|
||||||
{!props.plugin.compatible ? (
|
{!props.plugin.compatible ? (
|
||||||
<div class={classes.description}>
|
<div class={classes.description}>
|
||||||
{props.plugin.compatibilityMessage ?? "Plugin is not compatible."}
|
{props.plugin.compatibilityMessage ?? t["plugins-incompatible"]}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,8 +49,9 @@ export function PluginCard(props: {
|
||||||
size={ButtonSizes.SMALL}
|
size={ButtonSizes.SMALL}
|
||||||
onClick={props.onReload}
|
onClick={props.onReload}
|
||||||
disabled={props.busy || !props.plugin.enabled}
|
disabled={props.busy || !props.plugin.enabled}
|
||||||
|
tooltip={t["plugins-reload"]}
|
||||||
>
|
>
|
||||||
Reload
|
{t["plugins-reload"]}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
.header {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-muted, var(--text-subtle));
|
||||||
|
max-width: 42rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
28
src/shelter/settings/components/SettingsPageHeader.tsx
Normal file
28
src/shelter/settings/components/SettingsPageHeader.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/shelter/settings/components/SettingsPanel.module.css
Normal file
111
src/shelter/settings/components/SettingsPanel.module.css
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
.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);
|
||||||
|
}
|
||||||
83
src/shelter/settings/components/SettingsPanel.tsx
Normal file
83
src/shelter/settings/components/SettingsPanel.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
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 Shelter’s 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/shelter/settings/components/SettingsSearch.tsx
Normal file
26
src/shelter/settings/components/SettingsSearch.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ export const ThemesCard = (props: { theme: ThemeManifest }) => {
|
||||||
<div class={classes.card}>
|
<div class={classes.card}>
|
||||||
<div class={classes.topRow}>
|
<div class={classes.topRow}>
|
||||||
<div class={classes.info}>
|
<div class={classes.info}>
|
||||||
<Header tag={HeaderTags.H2} class={classes.title}>
|
<Header tag={HeaderTags.HeadingLG} class={classes.title}>
|
||||||
{props.theme.name}
|
{props.theme.name}
|
||||||
</Header>
|
</Header>
|
||||||
<div class={classes.author}>
|
<div class={classes.author}>
|
||||||
|
|
@ -66,18 +66,23 @@ export const ThemesCard = (props: { theme: ThemeManifest }) => {
|
||||||
</Show>
|
</Show>
|
||||||
<div class={classes.actions}>
|
<div class={classes.actions}>
|
||||||
<button
|
<button
|
||||||
title={store.i18n["themes-delete"]}
|
use:tooltip={store.i18n["themes-delete"]}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={removeTheme}
|
onClick={removeTheme}
|
||||||
class={`${classes.btn} ${classes.btnDanger}`}
|
class={`${classes.btn} ${classes.btnDanger}`}
|
||||||
>
|
>
|
||||||
<img class={classes.icon} alt={store.i18n["themes-delete"]} src="legcord://assets/Trash.png" />
|
<img class={classes.icon} alt={store.i18n["themes-delete"]} src="legcord://assets/Trash.png" />
|
||||||
</button>
|
</button>
|
||||||
<button title={store.i18n["themes-edit"]} type="button" onClick={editTheme} class={classes.btn}>
|
<button use:tooltip={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" />
|
<img class={classes.icon} alt={store.i18n["themes-edit"]} src="legcord://assets/Edit.png" />
|
||||||
</button>
|
</button>
|
||||||
<Show when={showUpdate}>
|
<Show when={showUpdate}>
|
||||||
<button title={store.i18n["themes-update"]} type="button" onClick={updateTheme} class={classes.btn}>
|
<button
|
||||||
|
use:tooltip={store.i18n["themes-update"]}
|
||||||
|
type="button"
|
||||||
|
onClick={updateTheme}
|
||||||
|
class={classes.btn}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
class={classes.icon}
|
class={classes.icon}
|
||||||
alt={store.i18n["themes-update"]}
|
alt={store.i18n["themes-update"]}
|
||||||
|
|
@ -85,7 +90,12 @@ export const ThemesCard = (props: { theme: ThemeManifest }) => {
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
<button title={store.i18n["themes-open"]} type="button" onClick={openThemesFolder} class={classes.btn}>
|
<button
|
||||||
|
use:tooltip={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" />
|
<img class={classes.icon} alt={store.i18n["themes-open"]} src="legcord://assets/Folder.png" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
111
src/shelter/settings/components/icons/SettingsPanelIcons.tsx
Normal file
111
src/shelter/settings/components/icons/SettingsPanelIcons.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* Category icons for settings panels.
|
||||||
|
* Filled 24×24 paths match Discord’s 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>
|
||||||
|
);
|
||||||
12
src/shelter/settings/pages/KeybindsPage.module.css
Normal file
12
src/shelter/settings/pages/KeybindsPage.module.css
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,49 @@
|
||||||
const {
|
import { For, Show } from "solid-js";
|
||||||
plugin: { store },
|
|
||||||
ui: { Button, Header, HeaderTags, ButtonSizes, Divider, openModal },
|
|
||||||
} = shelter;
|
|
||||||
|
|
||||||
import { For } from "solid-js";
|
|
||||||
import type { Keybind } from "../../../@types/keybind.js";
|
import type { Keybind } from "../../../@types/keybind.js";
|
||||||
|
import { EmptyState } from "../components/EmptyState.jsx";
|
||||||
import { KeybindCard } from "../components/KeybindCard.jsx";
|
import { KeybindCard } from "../components/KeybindCard.jsx";
|
||||||
import { KeybindMaker } from "../components/KeybindMaker.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 },
|
||||||
|
} = shelter;
|
||||||
|
|
||||||
export function KeybindsPage() {
|
export function KeybindsPage() {
|
||||||
function addNewKeybind() {
|
function addNewKeybind() {
|
||||||
openModal(({ close }: { close: () => void }) => <KeybindMaker close={close} />);
|
openModal(({ close }: { close: () => void }) => <KeybindMaker close={close} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const t = store.i18n;
|
||||||
|
const keybinds = () => (store.settings.keybinds as Keybind[]) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header tag={HeaderTags.H1}>Keybinds</Header>
|
<SettingsPageHeader title={t["keybinds-pageTitle"]} description={t["keybinds-pageDesc"]}>
|
||||||
<Divider mt mb />
|
<Text tag={TextTags.textSM} class={classes.tip}>
|
||||||
<div style={{ display: "flex", "justify-content": "flex-end", "margin-bottom": "12px" }}>
|
{t["keybind-globalNote"]}
|
||||||
|
</Text>
|
||||||
|
</SettingsPageHeader>
|
||||||
|
<div class={classes.toolbar}>
|
||||||
<Button size={ButtonSizes.LARGE} onClick={addNewKeybind}>
|
<Button size={ButtonSizes.LARGE} onClick={addNewKeybind}>
|
||||||
Create a keybind
|
{t["keybind-addKeybind"]}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<For each={store.settings.keybinds}>{(keybind: Keybind) => <KeybindCard keybind={keybind} />}</For>
|
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
src/shelter/settings/pages/PluginsPage.module.css
Normal file
6
src/shelter/settings/pages/PluginsPage.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import { createSignal, For, onMount, Show } from "solid-js";
|
import { createSignal, For, onMount, Show } from "solid-js";
|
||||||
import type { LegcordPluginInfo } from "../../../@types/legcordWindow.js";
|
import type { LegcordPluginInfo } from "../../../@types/legcordWindow.js";
|
||||||
|
import { EmptyState } from "../components/EmptyState.jsx";
|
||||||
import { PluginCard } from "../components/PluginCard.jsx";
|
import { PluginCard } from "../components/PluginCard.jsx";
|
||||||
|
import { SettingsPageHeader } from "../components/SettingsPageHeader.jsx";
|
||||||
|
import classes from "./PluginsPage.module.css";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ui: { Header, HeaderTags, Divider, Button, ButtonSizes, showToast },
|
ui: { Button, ButtonSizes, showToast },
|
||||||
|
plugin: { store },
|
||||||
} = shelter;
|
} = shelter;
|
||||||
|
|
||||||
|
function formatPluginMessage(template: string, name: string) {
|
||||||
|
return template.replaceAll("{name}", name);
|
||||||
|
}
|
||||||
|
|
||||||
export function PluginsPage() {
|
export function PluginsPage() {
|
||||||
const [plugins, setPlugins] = createSignal<LegcordPluginInfo[]>([]);
|
const [plugins, setPlugins] = createSignal<LegcordPluginInfo[]>([]);
|
||||||
const [busyIds, setBusyIds] = createSignal<string[]>([]);
|
const [busyIds, setBusyIds] = createSignal<string[]>([]);
|
||||||
|
const t = () => store.i18n;
|
||||||
|
|
||||||
const setBusy = (pluginId: string, state: boolean) => {
|
const setBusy = (pluginId: string, state: boolean) => {
|
||||||
setBusyIds((current) => {
|
setBusyIds((current) => {
|
||||||
|
|
@ -31,9 +40,10 @@ export function PluginsPage() {
|
||||||
try {
|
try {
|
||||||
if (enabled && !plugin.compatible) {
|
if (enabled && !plugin.compatible) {
|
||||||
showToast({
|
showToast({
|
||||||
title: "Plugins",
|
title: t()["plugins-toastTitle"],
|
||||||
content:
|
content:
|
||||||
plugin.compatibilityMessage ?? `${plugin.name} is not compatible with this Legcord version.`,
|
plugin.compatibilityMessage ??
|
||||||
|
formatPluginMessage(t()["plugins-toastIncompatible"], plugin.name),
|
||||||
duration: 3500,
|
duration: 3500,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -41,8 +51,11 @@ export function PluginsPage() {
|
||||||
const result = await window.legcord.plugins.setEnabled(plugin.id, enabled);
|
const result = await window.legcord.plugins.setEnabled(plugin.id, enabled);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showToast({
|
showToast({
|
||||||
title: "Plugins",
|
title: t()["plugins-toastTitle"],
|
||||||
content: `Failed to ${enabled ? "enable" : "disable"} ${plugin.name}.`,
|
content: formatPluginMessage(
|
||||||
|
enabled ? t()["plugins-toastEnableFailed"] : t()["plugins-toastDisableFailed"],
|
||||||
|
plugin.name,
|
||||||
|
),
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +70,11 @@ export function PluginsPage() {
|
||||||
try {
|
try {
|
||||||
const result = await window.legcord.plugins.reload(plugin.id);
|
const result = await window.legcord.plugins.reload(plugin.id);
|
||||||
showToast({
|
showToast({
|
||||||
title: "Plugins",
|
title: t()["plugins-toastTitle"],
|
||||||
content: result.ok ? `Reloaded ${plugin.name}.` : `Failed to reload ${plugin.name}.`,
|
content: formatPluginMessage(
|
||||||
|
result.ok ? t()["plugins-toastReloaded"] : t()["plugins-toastReloadFailed"],
|
||||||
|
plugin.name,
|
||||||
|
),
|
||||||
duration: 2500,
|
duration: 2500,
|
||||||
});
|
});
|
||||||
await refreshPlugins();
|
await refreshPlugins();
|
||||||
|
|
@ -73,19 +89,26 @@ export function PluginsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header tag={HeaderTags.H1}>Plugins</Header>
|
<SettingsPageHeader title={t()["plugins-pageTitle"]} description={t()["plugins-pageDesc"]} />
|
||||||
<Divider mt mb />
|
<div class={classes.toolbar}>
|
||||||
<div style={{ display: "flex", gap: "8px", "margin-bottom": "12px" }}>
|
|
||||||
<Button size={ButtonSizes.LARGE} onClick={() => void refreshPlugins()}>
|
<Button size={ButtonSizes.LARGE} onClick={() => void refreshPlugins()}>
|
||||||
Refresh List
|
{t()["plugins-refresh"]}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size={ButtonSizes.LARGE} onClick={window.legcord.plugins.openFolder}>
|
<Button size={ButtonSizes.LARGE} onClick={window.legcord.plugins.openFolder}>
|
||||||
Open Plugins Folder
|
{t()["plugins-openFolder"]}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Show when={plugins().length === 0}>
|
<Show
|
||||||
<Header tag={HeaderTags.H5}>No runtime plugins found in your plugins folder.</Header>
|
when={plugins().length > 0}
|
||||||
</Show>
|
fallback={
|
||||||
|
<EmptyState
|
||||||
|
message={t()["plugins-empty"]}
|
||||||
|
description={t()["plugins-emptyDesc"]}
|
||||||
|
actionLabel={t()["plugins-openFolder"]}
|
||||||
|
onAction={window.legcord.plugins.openFolder}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<For each={plugins()}>
|
<For each={plugins()}>
|
||||||
{(plugin) => (
|
{(plugin) => (
|
||||||
<PluginCard
|
<PluginCard
|
||||||
|
|
@ -96,6 +119,7 @@ export function PluginsPage() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</Show>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { sleep } from "../../../common/sleep.js";
|
||||||
import { AddDetectableModal } from "../components/AddDetectableModal.jsx";
|
import { AddDetectableModal } from "../components/AddDetectableModal.jsx";
|
||||||
import { DetectableCard } from "../components/DetectableCard.jsx";
|
import { DetectableCard } from "../components/DetectableCard.jsx";
|
||||||
import { Dropdown } from "../components/Dropdown.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";
|
import classes from "./RegisteredGames.module.css";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -93,8 +95,7 @@ export function RegisteredGamesPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header tag={HeaderTags.H1}>{t["games-registeredGames"]}</Header>
|
<SettingsPageHeader title={t["games-registeredGames"]} description={t["games-pageDesc"]} />
|
||||||
<Divider mt mb />
|
|
||||||
<div class={classes.addBox}>
|
<div class={classes.addBox}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
class={classes.dropdown}
|
class={classes.dropdown}
|
||||||
|
|
@ -128,27 +129,21 @@ export function RegisteredGamesPage() {
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
when={(detectables()?.length ?? 0) > 0}
|
when={(detectables()?.length ?? 0) > 0}
|
||||||
fallback={
|
fallback={<EmptyState message={t["games-empty"]} description={t["games-emptyDesc"]} />}
|
||||||
<Header tag={HeaderTags.H5} class={classes.empty}>
|
|
||||||
{t["games-empty"]}
|
|
||||||
</Header>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<For each={detectables()}>
|
<For each={detectables()}>
|
||||||
{(detectable) => <DetectableCard detectable={detectable} onRemove={refreshDetectables} />}
|
{(detectable) => <DetectableCard detectable={detectable} onRemove={refreshDetectables} />}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Header tag={HeaderTags.H3} class={classes.sectionHeader}>
|
<Header tag={HeaderTags.HeadingLG} class={classes.sectionHeader}>
|
||||||
{t["games-lastDetected"]}
|
{t["games-lastDetected"]}
|
||||||
</Header>
|
</Header>
|
||||||
<Divider mt mb />
|
<Divider mt mb />
|
||||||
<Show
|
<Show
|
||||||
when={lastDetected().length > 0}
|
when={lastDetected().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<Header tag={HeaderTags.H5} class={classes.empty}>
|
<EmptyState message={t["games-lastDetectedEmpty"]} description={t["games-lastDetectedEmptyDesc"]} />
|
||||||
{t["games-lastDetectedEmpty"]}
|
|
||||||
</Header>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ul class={classes.gameList}>
|
<ul class={classes.gameList}>
|
||||||
|
|
@ -171,16 +166,14 @@ export function RegisteredGamesPage() {
|
||||||
</ul>
|
</ul>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Header tag={HeaderTags.H3} class={classes.sectionHeader}>
|
<Header tag={HeaderTags.HeadingLG} class={classes.sectionHeader}>
|
||||||
{t["games-blacklisted"]}
|
{t["games-blacklisted"]}
|
||||||
</Header>
|
</Header>
|
||||||
<Divider mt mb />
|
<Divider mt mb />
|
||||||
<Show
|
<Show
|
||||||
when={blacklisted().length > 0}
|
when={blacklisted().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<Header tag={HeaderTags.H5} class={classes.empty}>
|
<EmptyState message={t["games-blacklistedEmpty"]} description={t["games-blacklistedEmptyDesc"]} />
|
||||||
{t["games-blacklistedEmpty"]}
|
|
||||||
</Header>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ul class={classes.gameList}>
|
<ul class={classes.gameList}>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,28 @@
|
||||||
.category {
|
.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 {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,19 @@
|
||||||
import { createSignal, For } from "solid-js";
|
import { createSignal, For, Show } from "solid-js";
|
||||||
import type { Settings } from "../../../@types/settings.js";
|
import type { Settings } from "../../../@types/settings.js";
|
||||||
import type { ThemeManifest } from "../../../@types/themeManifest.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 { ThemesCard } from "../components/ThemesCard.jsx";
|
||||||
import { refreshThemes, setConfig } from "../settings.js";
|
import { refreshThemes, setConfig } from "../settings.js";
|
||||||
import classes from "./ThemesPages.module.css";
|
import classes from "./ThemesPages.module.css";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ui: { Button, Header, HeaderTags, ButtonSizes, TextBox, showToast, SwitchItem },
|
ui: { Button, Header, HeaderTags, ButtonSizes, TextBox, showToast, SwitchItem, Text, TextTags },
|
||||||
plugin: { store },
|
plugin: { store },
|
||||||
} = shelter;
|
} = shelter;
|
||||||
|
|
||||||
|
const BETTERDISCORD_THEMES_URL = "https://betterdiscord.app/themes";
|
||||||
|
|
||||||
export function ThemesPage() {
|
export function ThemesPage() {
|
||||||
const [downloadUrl, setDownloadUrl] = createSignal("");
|
const [downloadUrl, setDownloadUrl] = createSignal("");
|
||||||
refreshThemes();
|
refreshThemes();
|
||||||
|
|
@ -28,9 +33,11 @@ export function ThemesPage() {
|
||||||
|
|
||||||
const settings = () => store.settings as Settings;
|
const settings = () => store.settings as Settings;
|
||||||
const t = store.i18n;
|
const t = store.i18n;
|
||||||
|
const themes = () => (store.themes as ThemeManifest[]) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header tag={HeaderTags.H1}>Themes</Header>
|
<SettingsPageHeader title={t["themes-pageTitle"]} description={t["themes-pageDesc"]} divider={true} />
|
||||||
<SwitchItem
|
<SwitchItem
|
||||||
note={store.i18n["settings-quickCss-desc"]}
|
note={store.i18n["settings-quickCss-desc"]}
|
||||||
value={settings().quickCss}
|
value={settings().quickCss}
|
||||||
|
|
@ -66,7 +73,7 @@ export function ThemesPage() {
|
||||||
store.themes = window.legcord.themes.refresh();
|
store.themes = window.legcord.themes.refresh();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t["themes-refresh"] ?? "Refresh"}
|
{t["themes-refresh"]}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class={classes.addBox}>
|
<div class={classes.addBox}>
|
||||||
|
|
@ -79,9 +86,24 @@ export function ThemesPage() {
|
||||||
{t["themes-import"]}
|
{t["themes-import"]}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Text tag={TextTags.textSM} class={classes.importNote}>
|
||||||
|
{t["themes-importUrlNote"]}
|
||||||
|
</Text>
|
||||||
<hr class={classes.divider} />
|
<hr class={classes.divider} />
|
||||||
<Header tag={HeaderTags.H3}>{t["themes-installed"]}</Header>
|
<Header tag={HeaderTags.HeadingLG}>{t["themes-installed"]}</Header>
|
||||||
<For each={store.themes}>{(theme: ThemeManifest) => <ThemesCard theme={theme} />}</For>
|
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,8 @@
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--background-modifier-accent);
|
border-top: 1px solid var(--background-modifier-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.importNote {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--text-muted, var(--text-subtle));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,75 @@ export function refreshThemes() {
|
||||||
store.themes = window.legcord.themes.getThemes();
|
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) {
|
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;
|
store.settings[key] = value;
|
||||||
console.log(key, ":", store.settings[key]);
|
console.log(key, ":", store.settings[key]);
|
||||||
if (shouldRestart) {
|
if (shouldRestart) {
|
||||||
isRestartRequired = true;
|
isRestartRequired = true;
|
||||||
}
|
}
|
||||||
window.legcord.settings.setConfig(key, value);
|
window.legcord.settings.setConfig(key, value);
|
||||||
refreshSettings();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMod(array: ValidMods[], filter: ValidMods) {
|
function removeMod(array: ValidMods[], filter: ValidMods) {
|
||||||
|
|
@ -34,14 +95,12 @@ function removeMod(array: ValidMods[], filter: ValidMods) {
|
||||||
|
|
||||||
export function toggleMod(mod: ValidMods, enabled: boolean) {
|
export function toggleMod(mod: ValidMods, enabled: boolean) {
|
||||||
isRestartRequired = true;
|
isRestartRequired = true;
|
||||||
const currentMods = store.settings.mods;
|
const currentMods = store.settings.mods as ValidMods[];
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (mod === "vencord") {
|
if (mod === "vencord") {
|
||||||
currentMods.push("vencord");
|
setConfig("mods", [...removeMod(currentMods, "equicord"), "vencord"]);
|
||||||
setConfig("mods", removeMod(currentMods, "equicord"));
|
|
||||||
} else if (mod === "equicord") {
|
} else if (mod === "equicord") {
|
||||||
currentMods.push("equicord");
|
setConfig("mods", [...removeMod(currentMods, "vencord"), "equicord"]);
|
||||||
setConfig("mods", removeMod(currentMods, "vencord"));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setConfig("mods", removeMod(currentMods, mod));
|
setConfig("mods", removeMod(currentMods, mod));
|
||||||
|
|
|
||||||
|
|
@ -66,19 +66,25 @@ function layerPop() {
|
||||||
|
|
||||||
export function onLoad() {
|
export function onLoad() {
|
||||||
log("Legcord Titlebar Controller");
|
log("Legcord Titlebar Controller");
|
||||||
switch (settings.windowStyle) {
|
if (settings.windowStyle === "default") {
|
||||||
case "default":
|
|
||||||
document.body.setAttribute("customTitlebar", "");
|
document.body.setAttribute("customTitlebar", "");
|
||||||
injectButtonControls();
|
injectButtonControls();
|
||||||
break;
|
return;
|
||||||
case "overlay":
|
}
|
||||||
|
|
||||||
|
// 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", "");
|
document.body.setAttribute("customTitlebar", "");
|
||||||
dispatcher.subscribe("LAYER_PUSH", layerPush);
|
dispatcher.subscribe("LAYER_PUSH", layerPush);
|
||||||
dispatcher.subscribe("LAYER_POP", layerPop);
|
dispatcher.subscribe("LAYER_POP", layerPop);
|
||||||
break;
|
return;
|
||||||
default:
|
|
||||||
log("Unsupported window style");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log("Unsupported window style");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onUnload() {
|
export function onUnload() {
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
/*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;
|
|
||||||
}
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Legcord</title>
|
<title>Legcord</title>
|
||||||
<style>
|
|
||||||
@import url("../css/splash.css");
|
|
||||||
</style>
|
|
||||||
<script>
|
<script>
|
||||||
window.onbeforeunload = () => {
|
window.onbeforeunload = () => {
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
|
|
@ -13,6 +10,137 @@
|
||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue