mirror of
https://github.com/smartfrigde/armcord.git
synced 2026-08-19 06:03:22 +00:00
Compare commits
96 commits
97942ebf9c
...
c8d91f6129
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d91f6129 | ||
|
|
79d444be19 |
||
|
|
0d4536e924 | ||
|
|
91fd2f6782 | ||
|
|
6a0f645df9 | ||
|
|
04d2b29f83 |
||
|
|
05a8438bd5 |
||
|
|
3ed5fca353 | ||
|
|
ee88b2dbc5 | ||
|
|
e9a95d4c75 | ||
|
|
c222657c30 | ||
|
|
9558af52dc | ||
|
|
acdec4a5e3 | ||
|
|
e60e46c88e | ||
|
|
71c6484a74 |
||
|
|
b2151296a4 |
||
|
|
73d26986e2 | ||
|
|
fd164d0b2f | ||
|
|
0d3f7b0008 | ||
|
|
f64eb87d70 | ||
|
|
20f557e721 | ||
|
|
758b97e63e | ||
|
|
0f7ac5449c | ||
|
|
bd3f8de6bc | ||
|
|
5d6cc64db7 | ||
|
|
d0dda79fbf |
||
|
|
642533f800 | ||
|
|
6ac0f48f4f | ||
|
|
d5641b5f0e | ||
|
|
38e5b20dfe | ||
|
|
f0ff4f9221 | ||
|
|
4fb59f7fff | ||
|
|
0599810505 | ||
|
|
853f3ff5e0 | ||
|
|
4e9da71366 | ||
|
|
bb7bf89312 | ||
|
|
464a1c2fb7 | ||
|
|
5d72d180b1 | ||
|
|
550662ce0c | ||
|
|
4ec80d11bb | ||
|
|
91a3d86a12 | ||
|
|
f30dadcdfd | ||
|
|
80689ea23b |
||
|
|
99281f3f05 |
||
|
|
a934aa50b1 | ||
|
|
5963a451ca | ||
|
|
1bedb9e431 |
||
|
|
d27b05f85a |
||
|
|
526d6dcce8 |
||
|
|
0d80100e44 |
||
|
|
4b8394f196 |
||
|
|
7e92800c12 |
||
|
|
dc7be47cbc |
||
|
|
69b8d4202e |
||
|
|
74231ca190 |
||
|
|
022676cf65 |
||
|
|
7b204d4481 |
||
|
|
d575d32865 |
||
|
|
0a022f1490 |
||
|
|
65b3dd5cec |
||
|
|
158f537e7d | ||
|
|
8aea77ba25 | ||
|
|
3ec1bc2d32 |
||
|
|
11d02104d9 |
||
|
|
7d933c6529 |
||
|
|
9347d4c7f8 | ||
|
|
b7582bf5af | ||
|
|
43ad9637ba |
||
|
|
6bd07ce067 |
||
|
|
9ac2f6b627 |
||
|
|
fbc9d72392 |
||
|
|
98eafab83c |
||
|
|
0d537effa9 |
||
|
|
b1876a11b6 |
||
|
|
bc3f46258c |
||
|
|
b886ef051e |
||
|
|
ef0007f0c9 |
||
|
|
089cff6792 |
||
|
|
5c4e9d0707 |
||
|
|
d68a322cc4 |
||
|
|
527e0b4298 | ||
|
|
09432f60bc | ||
|
|
dcfc37c97a | ||
|
|
24e800a41a | ||
|
|
12843004f4 |
||
|
|
eb0ddc3337 |
||
|
|
62934670ee |
||
|
|
e55cd408f7 |
||
|
|
92a3006725 | ||
|
|
d27acca0e0 | ||
|
|
fcce5f8146 | ||
|
|
a07d481d49 |
||
|
|
88dd287d7e |
||
|
|
355072866b |
||
|
|
b3d18900a9 |
||
|
|
fdee9ecd44 |
165 changed files with 10756 additions and 4479 deletions
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
## Summary
|
||||
|
||||
<!-- Briefly describe what this PR changes. -->
|
||||
|
||||
## Problem
|
||||
|
||||
<!-- What issue, bug, feature request, or maintenance task does this address? -->
|
||||
|
||||
## Solution
|
||||
|
||||
<!-- What changed, and why is this the right approach? -->
|
||||
|
||||
## Risk
|
||||
|
||||
<!-- Call out anything that could regress, be platform-specific, or require extra review. -->
|
||||
|
||||
## Testing
|
||||
|
||||
<!-- Describe the checks you ran and any manual smoke tests. -->
|
||||
|
||||
- [ ] `pnpm lint`
|
||||
- [ ] `pnpm build` if this PR touches runtime, bundling, or packaging code
|
||||
- [ ] Manual verification for affected flows
|
||||
- [ ] Screenshots or recordings for UI changes
|
||||
|
||||
## AI Notice
|
||||
|
||||
<!-- Please answer truthfully. AI/LLM tools include programs like Claude, Codex, and Cursor -->
|
||||
|
||||
- [ ] I have used any AI/LLM tool to generate code or text in this PR. I understand that I am responsible for reviewing and validating any code generated by AI, and that I will be held accountable for any issues caused by this code.
|
||||
- [ ] I have not used any AI to generate code in this PR.
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Add links to related issues, screenshots, follow-up work, or implementation details. -->
|
||||
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
|
|
@ -1,13 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: "13:00"
|
||||
open-pull-requests-limit: 99
|
||||
versioning-strategy: increase
|
||||
28
.github/workflows/flatpak-node.yml
vendored
Normal file
28
.github/workflows/flatpak-node.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
name: Upload generated-sources.json to release for Flatpak building
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install flatpak-node-generator
|
||||
run: pipx install git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node
|
||||
|
||||
- name: Create generated-sources.json
|
||||
run: /root/.local/bin/flatpak-node-generator pnpm pnpm-lock.yaml --node-sdk-extension org.freedesktop.Sdk.Extension.node26 --electron-node-headers
|
||||
|
||||
- name: Upload generated-sources.json to release
|
||||
run: |
|
||||
gh release upload ${{ github.event.release.tag_name }} generated-sources.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
|
@ -7,10 +7,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@v2
|
||||
with:
|
||||
version: 1.9.4
|
||||
- name: Run Biome
|
||||
run: biome ci . --reporter=github
|
||||
|
|
|
|||
6
.github/workflows/meta.yml
vendored
6
.github/workflows/meta.yml
vendored
|
|
@ -16,11 +16,11 @@ jobs:
|
|||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v6 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
|
|
|
|||
19
.github/workflows/package.yml
vendored
19
.github/workflows/package.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Add commit to version
|
||||
if: github.ref_name == 'dev'
|
||||
|
|
@ -25,10 +25,10 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Prepare PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Prepare Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
|
|
@ -44,13 +44,13 @@ jobs:
|
|||
uses: samuelmeuli/action-snapcraft@v3
|
||||
|
||||
- name: Load Electron cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .cache
|
||||
key: electron-zips.${{matrix.os}}
|
||||
|
||||
# Sadly, it makes more sense to separate builds per platform
|
||||
- name: Build Electron for MacOS (DMG & ZIP)
|
||||
- name: Build Electron for macOS (DMG & ZIP)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: pnpm electron-builder --universal -m zip dmg
|
||||
env:
|
||||
|
|
@ -69,19 +69,18 @@ jobs:
|
|||
run: pnpm electron-builder --armv7l --arm64 --x64 -l rpm deb appimage zip tar.gz
|
||||
|
||||
- name: Save Electron Cache
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: .cache
|
||||
key: electron-zips.${{matrix.os}}
|
||||
|
||||
- name: Collect artifacts
|
||||
run:
|
||||
mkdir artifacts |
|
||||
run: mkdir artifacts |
|
||||
find dist/. -maxdepth 1 -type f -exec mv {} artifacts \;
|
||||
shell: bash
|
||||
|
||||
- name: Upload artifactsstable
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{matrix.os}}-artifacts
|
||||
path: artifacts/*
|
||||
|
|
@ -94,7 +93,7 @@ jobs:
|
|||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: release-files
|
||||
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,4 +6,4 @@ package-lock.json
|
|||
.pnpm-store
|
||||
.cache
|
||||
.DS_Store
|
||||
pnpm-workspace.yaml
|
||||
scripts/spdx-license.txt
|
||||
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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
114
CONTRIBUTING.md
Normal file
114
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Contributing to Legcord
|
||||
|
||||
Legcord is an opinionated project. We welcome contributions from everyone, but we want your effort to land successfully and match the direction of the project.
|
||||
|
||||
This guide explains what maintainers look for in pull requests, based on existing code patterns and CI rules.
|
||||
|
||||
## How to contribute
|
||||
|
||||
Contributions are accepted through pull requests.
|
||||
|
||||
Pull requests should target the `dev` branch.
|
||||
If you are fixing a critical bug or security issue, you may open fixes for both `stable` and `dev`.
|
||||
|
||||
## Before you start a feature
|
||||
|
||||
- Check open pull requests for overlap.
|
||||
- Check [GitHub enhancement issues](https://github.com/Legcord/Legcord/issues?q=label%3Aenhancement) and the [feature requests Discord channel](https://discord.com/channels/820732039253852171/1261471243123818566).
|
||||
- If the feature does not exist yet, [open an issue](https://github.com/Legcord/Legcord/issues), say you want to implement it, and wait for feedback before writing large changes.
|
||||
- Familiarize yourself with the codebase rules below.
|
||||
|
||||
## Codebase rules
|
||||
|
||||
### Tooling and quality
|
||||
|
||||
- Use modern ESM (not CommonJS aka require) where possible.
|
||||
- Use pnpm (see `packageManager` in `package.json`).
|
||||
- Run `pnpm lint` before opening a PR.
|
||||
- Run `pnpm build` when your change touches runtime code, build output, bundling, or packaging behavior.
|
||||
- Avoid introducing new dependencies unless absolutely necessary.
|
||||
|
||||
### TypeScript and linting expectations
|
||||
|
||||
- Keep TypeScript strictness intact. Do not weaken compiler settings.
|
||||
- Avoid `@ts-ignore` and broad type escapes. If suppression is unavoidable, use the narrowest suppression possible and include a short reason.
|
||||
- Any lint suppression (`biome-ignore`, `eslint-disable`, etc.) must include a clear explanation and preferably a tracking issue/URL when relevant.
|
||||
|
||||
### Electron security expectations
|
||||
|
||||
- Follow Electron security best practices.
|
||||
- Keep preload APIs minimal and explicit when exposing APIs through `contextBridge`.
|
||||
- Do not add broad permissions or relax security defaults without a strong reason.
|
||||
- Be careful with injected JavaScript and user-controlled strings. Sanitize inputs (for example, use safe serialization patterns).
|
||||
- Be extra cautious when changing CSP handling, protocol handling, permission handlers, or web request hooks.
|
||||
|
||||
### Performance expectations
|
||||
|
||||
- Preserve existing performance patterns (config/lang/theme/window-state caching, debounce behavior, startup order).
|
||||
- Avoid changes that add repeated synchronous I/O in hot paths.
|
||||
- Be careful with startup flow: some existing operations intentionally use `void` or deferred execution to avoid startup hangs.
|
||||
|
||||
### Config and migration expectations
|
||||
|
||||
- If you change settings shape/defaults, update related migration logic and defaults together.
|
||||
- Keep backward compatibility with older config formats where possible.
|
||||
- If your PR changes user data behavior, explain migration and fallback behavior in the PR description.
|
||||
|
||||
### Cross-platform expectations
|
||||
|
||||
- Legcord ships on Linux, macOS, and Windows. Keep platform-specific behavior safe and scoped.
|
||||
- If your change is platform-specific, explicitly mention tested platform(s) in your PR.
|
||||
- If you cannot test a platform, state that clearly so reviewers know what remains unverified.
|
||||
|
||||
### i18n expectations
|
||||
|
||||
- Add manual translation changes only to `assets/lang/en-US.json`.
|
||||
- Update other translations on [Weblate](https://hosted.weblate.org/projects/armcord/).
|
||||
|
||||
## Pull request rules
|
||||
|
||||
### Scope and size
|
||||
|
||||
- Keep PRs focused on one concern.
|
||||
- Split unrelated refactors from feature or bugfix work.
|
||||
- Prefer small, reviewable PRs over large mixed changes.
|
||||
|
||||
### PR description (required)
|
||||
|
||||
Include these sections in your PR body:
|
||||
|
||||
- Problem: what user or developer issue is being solved.
|
||||
- Solution: what changed and why this approach was chosen.
|
||||
- Risk: what might regress.
|
||||
- Validation: what you tested (`pnpm lint`, `pnpm build`, manual scenarios, and platform coverage).
|
||||
|
||||
### Verification checklist
|
||||
|
||||
Before requesting review, verify:
|
||||
|
||||
- `pnpm lint` passes.
|
||||
- `pnpm build` passes for runtime/build-affecting changes.
|
||||
- Manual smoke test for affected flows is done (for example: startup, settings save/load, themes/mods behavior, tray/window behavior, CSP/security-sensitive paths if touched).
|
||||
- For UI changes, include screenshots or short recordings.
|
||||
|
||||
### Review readiness
|
||||
|
||||
- Mark breaking changes clearly.
|
||||
- Call out follow-up work explicitly if a workaround is temporary.
|
||||
- Reference related issues and prior discussions.
|
||||
- If you added a workaround/suppression, include the reason in code comments and in the PR description.
|
||||
|
||||
## AI Notice
|
||||
|
||||
While we welcome AI-assisted development, we expect contributors to review and validate any code generated by AI tools. You are responsible for ensuring that any AI-generated code meets our quality, security, and performance standards.
|
||||
We don't allow vibecoded slop. There has to be at least slight human touch in the code you submit. If you use AI tools to generate code, please disclose it in your PR description. Don't lie about it. We can tell and we will not accept PRs that are entirely AI-generated.
|
||||
|
||||
## Documentation
|
||||
|
||||
We are still improving project and codebase documentation.
|
||||
If you have experience building docs systems or contributor docs, contact @smartfrigde on Discord.
|
||||
|
||||
## Help users in the Discord community
|
||||
|
||||
We have an open support channel in our [Discord community](https://discord.gg/F25bc4RYDt).
|
||||
Helping users there is always appreciated.
|
||||
|
|
@ -141,7 +141,7 @@ Legcord is also available in [Pi-Apps](https://github.com/Botspot/pi-apps).
|
|||
|
||||
### Compiling
|
||||
|
||||
Alternatively, you can run Legcord from source ([NodeJS](https://nodejs.dev) and [pnpm](https://pnpm.io/installation#using-npm)) are required:
|
||||
Alternatively, you can run Legcord from source ([NodeJS v26 and newer](https://nodejs.dev) and [pnpm](https://pnpm.io/installation#using-npm)) are required:
|
||||
|
||||
1. Clone Legcord repo: `git clone https://github.com/Legcord/Legcord.git`
|
||||
2. Run `pnpm install` to install dependencies
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
clear: both;
|
||||
height: var(--custom-app-top-bar-height);
|
||||
line-height: 30px;
|
||||
-webkit-app-region: drag !important;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
background-color: var(--background-base-lowest);
|
||||
-webkit-user-select: none;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
[legcord-platform="darwin"] .sidebar-1tnWFu {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
|
||||
.platform-osx .wrapper_ef3116 {
|
||||
margin-top: 0px !important;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] #window-controls-container {
|
||||
|
|
@ -16,45 +16,6 @@
|
|||
transform: translate(-82px, 0px);
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] #window-controls-container:hover #minimize #minimize-icon,
|
||||
[legcord-platform="darwin"] #window-controls-container:hover #maximize #maximize-icon,
|
||||
[legcord-platform="darwin"] #window-controls-container:hover #quit #quit-icon {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"][unFocused] #window-controls-container #minimize,
|
||||
[legcord-platform="darwin"][unFocused] #window-controls-container #maximize,
|
||||
[legcord-platform="darwin"][unFocused] #window-controls-container #quit {
|
||||
background-color: #d6d6d5 !important;
|
||||
pointer-events: none;
|
||||
transition: background-color 0.1s ease-in;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"]:not([unFocused]) #window-controls-container #quit #quit-icon {
|
||||
background-color: #79282b;
|
||||
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNS4yOTI4OSA2TDIuODE4MDEgMy41MjUxM0wzLjUyNTEyIDIuODE4MDJMNS45OTk5OSA1LjI5Mjg5TDguNDc0ODcgMi44MTgwMkw5LjE4MTk3IDMuNTI1MTNMNi43MDcxIDZMOS4xODE5NyA4LjQ3NDg3TDguNDc0ODcgOS4xODE5OEw1Ljk5OTk5IDYuNzA3MTFMMy41MjUxMiA5LjE4MTk4TDIuODE4MDEgOC40NzQ4N0w1LjI5Mjg5IDZaIiBmaWxsPSJyZ2JhKDEyOCwgNiwgMCwgMSkiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNS4yOTI4OSA2TDIuODE4MDEgMy41MjUxM0wzLjUyNTEyIDIuODE4MDJMNS45OTk5OSA1LjI5Mjg5TDguNDc0ODcgMi44MTgwMkw5LjE4MTk3IDMuNTI1MTNMNi43MDcxIDZMOS4xODE5NyA4LjQ3NDg3TDguNDc0ODcgOS4xODE5OEw1Ljk5OTk5IDYuNzA3MTFMMy41MjUxMiA5LjE4MTk4TDIuODE4MDEgOC40NzQ4N0w1LjI5Mjg5IDZaIiBmaWxsPSJyZ2JhKDEyOCwgNiwgMCwgMSkiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
transform: translate(-0.4px, -7px);
|
||||
}
|
||||
[legcord-platform="darwin"]:not([unFocused]) #window-controls-container #minimize #minimize-icon {
|
||||
background-color: #7d631b;
|
||||
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAgNS4zOTk5OUgyVjYuNTk5OTlIMTBWNS4zOTk5OVoiIGZpbGw9IiM5ODY4MDEiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAgNS4zOTk5OUgyVjYuNTk5OTlIMTBWNS4zOTk5OVoiIGZpbGw9IiM5ODY4MDEiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
transform: translate(-0px, -7px);
|
||||
}
|
||||
[legcord-platform="darwin"]:not([unFocused]) #window-controls-container #maximize #maximize-icon {
|
||||
background-color: #1d7525;
|
||||
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOC41ODgyMyA2Ljk5MDE1TDUuMDA5NzkgMy40MTE3QzQuODU1ODMgMy4yNTc3NCA0Ljk1ODYgMi45OTQyMiA1LjE3NjE0IDIuOTg1MTRMOC45MTA0MiAyLjgyOTMxQzkuMDU2NTggMi44MjMyMSA5LjE3NjczIDIuOTQzMzUgOS4xNzA2MyAzLjA4OTUyTDkuMDE0NzkgNi44MjM4QzkuMDA1NzEgNy4wNDEzNCA4Ljc0MjE5IDcuMTQ0MTEgOC41ODgyMyA2Ljk5MDE1WiIgZmlsbD0iIzEyNUUxRSIvPjxwYXRoIGQ9Ik0zLjQxMTc3IDUuMDA5ODJMNi45OTAyMSA4LjU4ODI3QzcuMTQ0MTcgOC43NDIyMyA3LjA0MTQgOS4wMDU3NSA2LjgyMzg2IDkuMDE0ODNMMy4wODk1OCA5LjE3MDY2QzIuOTQzNDIgOS4xNzY3NiAyLjgyMzI3IDkuMDU2NjEgMi44MjkzNyA4LjkxMDQ1TDIuOTg1MjEgNS4xNzYxN0MyLjk5NDI5IDQuOTU4NjMgMy4yNTc4MSA0Ljg1NTg2IDMuNDExNzcgNS4wMDk4MloiIGZpbGw9IiMxMjVFMUUiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOC41ODgyMyA2Ljk5MDE1TDUuMDA5NzkgMy40MTE3QzQuODU1ODMgMy4yNTc3NCA0Ljk1ODYgMi45OTQyMiA1LjE3NjE0IDIuOTg1MTRMOC45MTA0MiAyLjgyOTMxQzkuMDU2NTggMi44MjMyMSA5LjE3NjczIDIuOTQzMzUgOS4xNzA2MyAzLjA4OTUyTDkuMDE0NzkgNi44MjM4QzkuMDA1NzEgNy4wNDEzNCA4Ljc0MjE5IDcuMTQ0MTEgOC41ODgyMyA2Ljk5MDE1WiIgZmlsbD0iIzEyNUUxRSIvPjxwYXRoIGQ9Ik0zLjQxMTc3IDUuMDA5ODJMNi45OTAyMSA4LjU4ODI3QzcuMTQ0MTcgOC43NDIyMyA3LjA0MTQgOS4wMDU3NSA2LjgyMzg2IDkuMDE0ODNMMy4wODk1OCA5LjE3MDY2QzIuOTQzNDIgOS4xNzY3NiAyLjgyMzI3IDkuMDU2NjEgMi44MjkzNyA4LjkxMDQ1TDIuOTg1MjEgNS4xNzYxN0MyLjk5NDI5IDQuOTU4NjMgMy4yNTc4MSA0Ljg1NTg2IDMuNDExNzcgNS4wMDk4MloiIGZpbGw9IiMxMjVFMUUiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
transform: translate(0px, -7px);
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] #window-controls-container #minimize {
|
||||
background-color: #fac536;
|
||||
transition: background-color 0.1s ease-in;
|
||||
|
|
@ -116,6 +77,45 @@
|
|||
}
|
||||
|
||||
div[class*="bar__"][class*="hidden__"] {
|
||||
pointer-events: unset !important;
|
||||
visibility: unset !important;
|
||||
pointer-events: unset;
|
||||
visibility: unset;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] #window-controls-container:hover #minimize #minimize-icon,
|
||||
[legcord-platform="darwin"] #window-controls-container:hover #maximize #maximize-icon,
|
||||
[legcord-platform="darwin"] #window-controls-container:hover #quit #quit-icon {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"][unFocused] #window-controls-container #minimize,
|
||||
[legcord-platform="darwin"][unFocused] #window-controls-container #maximize,
|
||||
[legcord-platform="darwin"][unFocused] #window-controls-container #quit {
|
||||
background-color: #d6d6d5;
|
||||
pointer-events: none;
|
||||
transition: background-color 0.1s ease-in;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"]:not([unFocused]) #window-controls-container #quit #quit-icon {
|
||||
background-color: #79282b;
|
||||
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNS4yOTI4OSA2TDIuODE4MDEgMy41MjUxM0wzLjUyNTEyIDIuODE4MDJMNS45OTk5OSA1LjI5Mjg5TDguNDc0ODcgMi44MTgwMkw5LjE4MTk3IDMuNTI1MTNMNi43MDcxIDZMOS4xODE5NyA4LjQ3NDg3TDguNDc0ODcgOS4xODE5OEw1Ljk5OTk5IDYuNzA3MTFMMy41MjUxMiA5LjE4MTk4TDIuODE4MDEgOC40NzQ4N0w1LjI5Mjg5IDZaIiBmaWxsPSJyZ2JhKDEyOCwgNiwgMCwgMSkiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNS4yOTI4OSA2TDIuODE4MDEgMy41MjUxM0wzLjUyNTEyIDIuODE4MDJMNS45OTk5OSA1LjI5Mjg5TDguNDc0ODcgMi44MTgwMkw5LjE4MTk3IDMuNTI1MTNMNi43MDcxIDZMOS4xODE5NyA4LjQ3NDg3TDguNDc0ODcgOS4xODE5OEw1Ljk5OTk5IDYuNzA3MTFMMy41MjUxMiA5LjE4MTk4TDIuODE4MDEgOC40NzQ4N0w1LjI5Mjg5IDZaIiBmaWxsPSJyZ2JhKDEyOCwgNiwgMCwgMSkiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
transform: translate(-0.4px, -7px);
|
||||
}
|
||||
[legcord-platform="darwin"]:not([unFocused]) #window-controls-container #minimize #minimize-icon {
|
||||
background-color: #7d631b;
|
||||
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAgNS4zOTk5OUgyVjYuNTk5OTlIMTBWNS4zOTk5OVoiIGZpbGw9IiM5ODY4MDEiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAgNS4zOTk5OUgyVjYuNTk5OTlIMTBWNS4zOTk5OVoiIGZpbGw9IiM5ODY4MDEiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
transform: translate(-0px, -7px);
|
||||
}
|
||||
[legcord-platform="darwin"]:not([unFocused]) #window-controls-container #maximize #maximize-icon {
|
||||
background-color: #1d7525;
|
||||
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOC41ODgyMyA2Ljk5MDE1TDUuMDA5NzkgMy40MTE3QzQuODU1ODMgMy4yNTc3NCA0Ljk1ODYgMi45OTQyMiA1LjE3NjE0IDIuOTg1MTRMOC45MTA0MiAyLjgyOTMxQzkuMDU2NTggMi44MjMyMSA5LjE3NjczIDIuOTQzMzUgOS4xNzA2MyAzLjA4OTUyTDkuMDE0NzkgNi44MjM4QzkuMDA1NzEgNy4wNDEzNCA4Ljc0MjE5IDcuMTQ0MTEgOC41ODgyMyA2Ljk5MDE1WiIgZmlsbD0iIzEyNUUxRSIvPjxwYXRoIGQ9Ik0zLjQxMTc3IDUuMDA5ODJMNi45OTAyMSA4LjU4ODI3QzcuMTQ0MTcgOC43NDIyMyA3LjA0MTQgOS4wMDU3NSA2LjgyMzg2IDkuMDE0ODNMMy4wODk1OCA5LjE3MDY2QzIuOTQzNDIgOS4xNzY3NiAyLjgyMzI3IDkuMDU2NjEgMi44MjkzNyA4LjkxMDQ1TDIuOTg1MjEgNS4xNzYxN0MyLjk5NDI5IDQuOTU4NjMgMy4yNTc4MSA0Ljg1NTg2IDMuNDExNzcgNS4wMDk4MloiIGZpbGw9IiMxMjVFMUUiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOC41ODgyMyA2Ljk5MDE1TDUuMDA5NzkgMy40MTE3QzQuODU1ODMgMy4yNTc3NCA0Ljk1ODYgMi45OTQyMiA1LjE3NjE0IDIuOTg1MTRMOC45MTA0MiAyLjgyOTMxQzkuMDU2NTggMi44MjMyMSA5LjE3NjczIDIuOTQzMzUgOS4xNzA2MyAzLjA4OTUyTDkuMDE0NzkgNi44MjM4QzkuMDA1NzEgNy4wNDEzNCA4Ljc0MjE5IDcuMTQ0MTEgOC41ODgyMyA2Ljk5MDE1WiIgZmlsbD0iIzEyNUUxRSIvPjxwYXRoIGQ9Ik0zLjQxMTc3IDUuMDA5ODJMNi45OTAyMSA4LjU4ODI3QzcuMTQ0MTcgOC43NDIyMyA3LjA0MTQgOS4wMDU3NSA2LjgyMzg2IDkuMDE0ODNMMy4wODk1OCA5LjE3MDY2QzIuOTQzNDIgOS4xNzY3NiAyLjgyMzI3IDkuMDU2NjEgMi44MjkzNyA4LjkxMDQ1TDIuOTg1MjEgNS4xNzYxN0MyLjk5NDI5IDQuOTU4NjMgMy4yNTc4MSA0Ljg1NTg2IDMuNDExNzcgNS4wMDk4MloiIGZpbGw9IiMxMjVFMUUiLz48L3N2Zz4=")
|
||||
no-repeat 50% 50%;
|
||||
transform: translate(0px, -7px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
}
|
||||
|
||||
div:has(> span [data-list-item-id="guildsnav___app-download-button"]) {
|
||||
display: none !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* custom keybinds are not supported on web information */
|
||||
.container__6436f.info__6436f.browserNotice__740f2 {
|
||||
visibility: hidden;
|
||||
display: block !important;
|
||||
display: block;
|
||||
}
|
||||
.container__6436f.info__6436f.browserNotice__740f2:after {
|
||||
content: "You can modify global keybinds using the keybind maker on the left sidebar";
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
@ -13,22 +13,6 @@
|
|||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #minimize:hover #minimize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #maximize:hover #maximize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:hover {
|
||||
background-color: #e81123;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:hover #quit-icon {
|
||||
background-color: #ffffff;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #minimize {
|
||||
background-color: transparent;
|
||||
transition: 0.1s ease;
|
||||
|
|
@ -41,14 +25,6 @@
|
|||
background-color: transparent;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:active {
|
||||
background-color: #f1707a;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:active #quit-icon {
|
||||
background-color: #000000cc;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
|
|
@ -57,6 +33,14 @@
|
|||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:active {
|
||||
background-color: #f1707a;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:active #quit-icon {
|
||||
background-color: #000000cc;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #minimize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
|
|
@ -73,6 +57,22 @@
|
|||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #minimize:hover #minimize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #maximize:hover #maximize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:hover {
|
||||
background-color: #e81123;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="linux"] #window-controls-container #quit:hover #quit-icon {
|
||||
background-color: #ffffff;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
|
||||
[legcord-platform="linux"][isMaximized] #window-controls-container #maximize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
clear: both;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
-webkit-app-region: drag !important;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
position: fixed;
|
||||
|
|
@ -58,6 +58,22 @@
|
|||
line-height: 45px;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
[legcord-platform="linux"] .titlebar #window-controls-container #minimize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
-webkit-mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="linux"] .titlebar #window-controls-container #maximize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
-webkit-mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="linux"] .titlebar #window-controls-container #minimize:hover {
|
||||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
|
|
@ -87,22 +103,6 @@
|
|||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="linux"] .titlebar #window-controls-container #minimize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
-webkit-mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="linux"] .titlebar #window-controls-container #maximize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
-webkit-mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='9' height='9' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
|
||||
[legcord-platform="linux"][isMaximized] .titlebar #window-controls-container #maximize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
|
|
@ -145,30 +145,6 @@
|
|||
[legcord-platform="win32"] .titlebar #window-controls-container {
|
||||
width: 142px;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #minimize:hover {
|
||||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #maximize:hover {
|
||||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #minimize:hover #minimize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #maximize:hover #maximize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #quit:hover {
|
||||
background-color: #e81123;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #quit:hover #quit-icon {
|
||||
background-color: #ffffff;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #minimize {
|
||||
background-color: transparent;
|
||||
transition: 0.1s ease;
|
||||
|
|
@ -189,6 +165,7 @@
|
|||
background-color: #000000cc;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
/* biome-ignore lint/style/noDescendingSpecificity: cross-platform, linux [legcord-platform="linux"] only */
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #quit-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
|
|
@ -213,6 +190,30 @@
|
|||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #minimize:hover {
|
||||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #maximize:hover {
|
||||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #minimize:hover #minimize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #maximize:hover #maximize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #quit:hover {
|
||||
background-color: #e81123;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] .titlebar #window-controls-container #quit:hover #quit-icon {
|
||||
background-color: #ffffff;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
|
||||
[legcord-platform="win32"][isMaximized] .titlebar #window-controls-container #maximize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
|
|
@ -226,11 +227,11 @@
|
|||
/* Legcord on macOS */
|
||||
|
||||
[legcord-platform="darwin"] .sidebar-1tnWFu {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] .container__037ed {
|
||||
overflow: unset !important;
|
||||
overflow: unset;
|
||||
padding-top: 48px;
|
||||
top: -48px;
|
||||
}
|
||||
|
|
@ -256,7 +257,7 @@
|
|||
[legcord-platform="darwin"][unFocused] .titlebar #window-controls-container #minimize,
|
||||
[legcord-platform="darwin"][unFocused] .titlebar #window-controls-container #maximize,
|
||||
[legcord-platform="darwin"][unFocused] .titlebar #window-controls-container #quit {
|
||||
background-color: #d6d6d5 !important;
|
||||
background-color: #d6d6d5;
|
||||
pointer-events: none;
|
||||
transition: background-color 0.1s ease-in;
|
||||
}
|
||||
|
|
@ -338,7 +339,11 @@
|
|||
|
||||
[legcord-platform="darwin"] .titlebar #window-controls-container #minimize-icon,
|
||||
[legcord-platform="darwin"] .titlebar #window-controls-container #maximize-icon,
|
||||
[legcord-platform="darwin"] .titlebar #window-controls-container #quit-icon {
|
||||
/* biome-ignore lint/style/noDescendingSpecificity: cross-platform */
|
||||
[legcord-platform="darwin"]
|
||||
.titlebar
|
||||
#window-controls-container
|
||||
#quit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -346,12 +351,6 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] .window-title {
|
||||
float: none;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
content: url("legcord://assets/Wordmark.png");
|
||||
height: 15px;
|
||||
|
|
@ -362,6 +361,12 @@
|
|||
filter: invert(30%);
|
||||
}
|
||||
|
||||
[legcord-platform="darwin"] .window-title {
|
||||
float: none;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
div#app-mount {
|
||||
height: calc(100% - 30px);
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -15,22 +15,6 @@
|
|||
background-color: var(--interactive-background-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #minimize:hover #minimize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #maximize:hover #maximize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:hover {
|
||||
background-color: #e81123;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:hover #quit-icon {
|
||||
background-color: #ffffff;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #minimize {
|
||||
background-color: transparent;
|
||||
transition: 0.1s ease;
|
||||
|
|
@ -43,14 +27,6 @@
|
|||
background-color: transparent;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:active {
|
||||
background-color: #f1707a;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:active #quit-icon {
|
||||
background-color: #000000cc;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
|
|
@ -59,6 +35,14 @@
|
|||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:active {
|
||||
background-color: #f1707a;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:active #quit-icon {
|
||||
background-color: #000000cc;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #minimize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
display: list-item;
|
||||
|
|
@ -75,6 +59,22 @@
|
|||
mask: url("data:image/svg+xml;charset=utf-8,<svg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/></svg>")
|
||||
no-repeat 50% 50%;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #minimize:hover #minimize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #maximize:hover #maximize-icon {
|
||||
background-color: var(--interactive-text-hover);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:hover {
|
||||
background-color: #e81123;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
[legcord-platform="win32"] #window-controls-container #quit:hover #quit-icon {
|
||||
background-color: #ffffff;
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
|
||||
[legcord-platform="win32"][isMaximized] #window-controls-container #maximize-icon {
|
||||
background-color: var(--interactive-icon-default);
|
||||
|
|
|
|||
BIN
assets/app/desktop.png
Normal file
BIN
assets/app/desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
File diff suppressed because one or more lines are too long
BIN
assets/app/overlay.png
Normal file
BIN
assets/app/overlay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
1
assets/app/shelter.svg
Normal file
1
assets/app/shelter.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="322" height="322" fill="none" viewBox="0 0 322 322"><g clip-path="url(#clip0_77_26)"><path fill="url(#paint0_linear_77_26)" d="M297 0H25C11.1929 0 0 11.1929 0 25V297C0 310.807 11.1929 322 25 322H297C310.807 322 322 310.807 322 297V25C322 11.1929 310.807 0 297 0Z"/><g filter="url(#filter0_d_77_26)"><path fill="#CCFFED" d="M99.4277 199.184C103.018 227.904 130.909 239.778 161.286 239.778C197.186 239.778 221.763 224.037 221.763 193.661C221.763 175.158 209.889 159.97 185.587 154.723L152.725 147.543C141.955 145.058 140.298 140.363 140.298 135.945C140.298 129.593 144.441 122.413 159.077 122.413C174.265 122.413 183.93 130.422 185.311 141.468L220.106 134.564C215.412 110.539 192.767 93.6935 160.181 93.6935C125.386 93.6935 103.846 112.748 103.846 139.259C103.846 159.142 117.93 173.225 142.784 178.472L170.951 184.271C182.826 186.757 186.14 191.728 186.14 196.698C186.14 204.431 178.131 210.506 164.6 210.506C150.24 210.506 138.089 205.811 136.156 191.728L99.4277 199.184Z"/></g></g><defs><filter id="filter0_d_77_26" width="122.335" height="153.085" x="99.428" y="93.694" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="7"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow_77_26"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_77_26" mode="normal" result="shape"/></filter><linearGradient id="paint0_linear_77_26" x1="161" x2="161" y1="0" y2="1105" gradientUnits="userSpaceOnUse"><stop stop-color="#2A3B4B"/><stop offset="1" stop-color="#2BFAAC"/></linearGradient><clipPath id="clip0_77_26"><rect width="322" height="322" fill="#fff"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -27,8 +27,8 @@
|
|||
"settings-transparency-universal": "Universell",
|
||||
"settings-transparency-modern": "Modern",
|
||||
"settings-theme-transparent": "Transparent",
|
||||
"settings-transparency-tahoe-warning": "Transparenz kann zu großem Lags auf MacOS 26 Tahoe führen.",
|
||||
"settings-popoutPiP": "Popup-Fenster immer im Vordergrund anzeigen",
|
||||
"settings-transparency-tahoe-warning": "Transparenz kann zu großem Lags auf macOS 26 Tahoe führen.",
|
||||
"settings-popoutPiP": "Anruf Popup-Fenster immer im Vordergrund anzeigen",
|
||||
"settings-popoutPiP-desc": "Wenn diese Option aktiviert ist, wird das Anruf-Popup im Modus „Immer im Vordergrund“ angezeigt.",
|
||||
"settings-venmic-workaround": "Umgehungslösung",
|
||||
"settings-venmic-workaround-desc": "Aktivieren oder deaktivieren Sie die Problemumgehung für ein Problem, das dazu führt, dass das Mikrofon anstelle des korrekten Audiosignals geteilt wird.",
|
||||
|
|
@ -52,8 +52,8 @@
|
|||
"settings-csp-desc": "Legcord CSP ist unser System, der das Laden von benutzerdefinierten Inhalten in die Discord-App verwaltet. Dinge wie Client-Mods und Designs hängen davon ab. Deaktivieren Sie es, wenn Sie Mods und benutzerdefinierte Stile entfernen möchten.",
|
||||
"settings-mintoTray": "Im Hintergrund arbeiten",
|
||||
"settings-mintoTray-desc": "Wenn diese Option deaktiviert ist, wird Legcord wie jedes andere Fenster geschlossen, wenn es geschlossen wird. Andernfalls bleibt es in Ihrer Taskleiste für später gespeichert.",
|
||||
"settings-startMinimized": "Minimiert starten",
|
||||
"settings-startMinimized-desc": "Legcord startet im Hintergrund und bleibt Ihnen nicht im Weg.",
|
||||
"settings-startMinimized": "Startfenster",
|
||||
"settings-startMinimized-desc": "Legt fest, wie Legcord beim Start erscheint. Tray-Modus blendet das Fenster vollständig aus (Tray-Symbol muss aktiviert sein).",
|
||||
"settings-useSystemCssEditor": "System-CSS-Editor verwenden",
|
||||
"settings-useSystemCssEditor-desc": "Verwenden Sie den System-CSS-Editor, um CSS zu bearbeiten.",
|
||||
"settings-MultiInstance": "Mehrfachinstanz",
|
||||
|
|
@ -89,7 +89,12 @@
|
|||
"settings-prfmMode": "Leistungsmodus",
|
||||
"settings-prfmMode-desc": "Der Leistungsmodus ist eine experimentelle Funktion in Legcord, die darauf ausgelegt ist, die Reaktionsfähigkeit und Leistung entsprechend Ihren Anforderungen zu optimieren. Die Auswirkungen können je nach Ihrer Hardware und Nutzung variieren. Wir empfehlen Ihnen daher, jeden Modus auszuprobieren, um herauszufinden, welcher für Sie am besten geeignet ist.",
|
||||
"settings-prfmMode-performance": "Leistung",
|
||||
"settings-prfmMode-balanced": "Ausgewogen",
|
||||
"settings-prfmMode-battery": "Batterie",
|
||||
"settings-prfmMode-memory": "Speichersparer",
|
||||
"settings-prfmMode-voip": "Sprache & Video",
|
||||
"settings-prfmMode-latency": "Niedrige Latenz",
|
||||
"settings-prfmMode-smoothScreenshare": "Flüssige Bildschirmfreigabe",
|
||||
"settings-prfmMode-dynamic": "Dynamisch",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Automatische Verstärkung deaktivieren",
|
||||
|
|
@ -229,5 +234,17 @@
|
|||
"games-removeConfirmHeader": "Spiel entfernen?",
|
||||
"games-noExecutables": "Keine ausführbaren Dateien",
|
||||
"games-lastDetected": "Zuletzt erkannte Spiele",
|
||||
"games-removeFromBlacklist": "Entfernen"
|
||||
"games-removeFromBlacklist": "Entfernen",
|
||||
"settings-material": "Fenster Material",
|
||||
"settings-material-acrylic": "Acryl",
|
||||
"settings-material-none": "Keine",
|
||||
"settings-csp-strict": "Strikt",
|
||||
"settings-csp-none": "Keine",
|
||||
"settings-csp-vanilla": "Vanilla",
|
||||
"settings-firstTimeCrash": "Wir richten Dinge für sie ein!",
|
||||
"settings-automaticClientUpdates": "Automatische client updates",
|
||||
"settings-automaticClientUpdates-desc": "Deaktiviert automatische Legcord Updates",
|
||||
"settings-startMinimized-off": "Normal",
|
||||
"settings-startMinimized-minimized": "Minimiert in die Taskleiste",
|
||||
"settings-startMinimized-tray": "Im Tray versteckt"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"settings-transparency-universal": "Universal",
|
||||
"settings-transparency-modern": "Modern",
|
||||
"settings-theme-transparent": "Transparent",
|
||||
"settings-transparency-tahoe-warning": "Transparency may cause excessive lag on MacOS 26 Tahoe.",
|
||||
"settings-transparency-tahoe-warning": "Transparency may cause excessive lag on macOS 26 Tahoe.",
|
||||
"settings-material": "Window Material",
|
||||
"settings-material-desc": "Set the Windows background material Legcord uses.",
|
||||
"settings-material-mica": "Mica",
|
||||
|
|
@ -57,13 +57,20 @@
|
|||
"settings-openCustomIconDialog": "Set desktop icon",
|
||||
"settings-mintoTray": "Work in background",
|
||||
"settings-mintoTray-desc": "When disabled, Legcord will close like any other window when closed, otherwise it'll sit back and relax in your system tray for later.",
|
||||
"settings-startMinimized": "Start minimized",
|
||||
"settings-startMinimized-desc": "Legcord starts in background and remains out of your way.",
|
||||
"settings-startMinimized": "Startup window",
|
||||
"settings-startMinimized-desc": "Choose how Legcord appears when it launches. Tray mode hides the window completely (needs a tray icon enabled).",
|
||||
"settings-startMinimized-off": "Normal",
|
||||
"settings-startMinimized-minimized": "Minimized to taskbar",
|
||||
"settings-startMinimized-tray": "Hidden in tray",
|
||||
"settings-csp": "Content Security Policy",
|
||||
"settings-csp-desc": "Set the strictness of Legcord's Content Security Policy. Strict CSP provides better security but may cause compatibility issues with some client mods/themes/plugins.",
|
||||
"settings-csp-strict": "Strict",
|
||||
"settings-csp-none": "None",
|
||||
"settings-csp-vanilla": "Vanilla",
|
||||
"settings-showExperimentalPluginMenu": "Show experimental plugin menu",
|
||||
"settings-showExperimentalPluginMenu-desc": "Show the experimental plugin menu in settings. This is a temporary setting until the plugin system is fully implemented.",
|
||||
"settings-firstTimeCrash": "We're setting things up for you!",
|
||||
"settings-firstTimeCrash-desc": "Settings are not available on a first-time launch. Please use a button below to restart and settings should be ready after the restart.",
|
||||
"settings-useSystemCssEditor": "Use system CSS editor",
|
||||
"settings-useSystemCssEditor-desc": "Use system CSS editor to edit CSS.",
|
||||
"settings-MultiInstance": "Multi Instance",
|
||||
|
|
@ -74,12 +81,14 @@
|
|||
"settings-automaticClientUpdates-desc": "Disables automatic Legcord updates",
|
||||
"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-sdpH264BaselineRewrite": "H.264 Baseline SDP rewrite",
|
||||
"settings-sdpH264BaselineRewrite-desc": "Rewrites Discord's Constrained Baseline H.264 profile in WebRTC SDP so screenshare can use hardware encode (VideoToolbox / Media Foundation) instead of OpenH264. Disable if Go Live fails to connect or video is broken, then restart Legcord.",
|
||||
"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-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-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-desc": "Prevent Legcord from being suspended. Keeps system active but allows screen to be turned off.",
|
||||
"settings-mobileMode": "Mobile mode",
|
||||
|
|
@ -87,7 +96,7 @@
|
|||
"settings-spellcheck": "Spellcheck",
|
||||
"settings-spellcheck-desc": "Helps you correct misspelled words by highlighting them.",
|
||||
"settings-vaapi": "VAAPI",
|
||||
"settings-vaapi-desc": "Use VAAPI (HW acceleration) for video decoding on Linux. This greatly reduces CPU usage during screenshare but may cause issues on some systems. Disable if you experience crashes or black screens during screenshare.",
|
||||
"settings-vaapi-desc": "Use VAAPI for hardware video encode and decode on Linux (sharer and viewer). Greatly reduces CPU during screenshare, but some GPUs (notably older AMD) produce frozen or blocky streams for viewers — disable to force software OpenH264 encode if that happens.",
|
||||
"settings-channel": "Discord channel",
|
||||
"settings-channel-desc": "Use this setting to change current instance of Discord that Legcord is running.",
|
||||
"settings-bitrateMin": "Minimum bitrate",
|
||||
|
|
@ -109,9 +118,14 @@
|
|||
"settings-mod-vencord": "Lightweight, and easy to use client mod. Features a built-in store for plugins.",
|
||||
"settings-mod-equicord": "Forked and born from vencord contributors, featuring a pretty plugin-rich client.",
|
||||
"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": "Optimizes responsiveness and GPU behavior. When Hardware Acceleration is on, Legcord always enables WebRTC hardware encode/decode for screenshare and calls; these modes add broader GPU, latency, or battery tradeoffs on top. Screenshare bitrate is auto-capped for stability — pick resolution and FPS in the share picker.",
|
||||
"settings-prfmMode-performance": "Performance",
|
||||
"settings-prfmMode-balanced": "Balanced",
|
||||
"settings-prfmMode-battery": "Battery",
|
||||
"settings-prfmMode-memory": "Memory saver",
|
||||
"settings-prfmMode-voip": "Voice & video",
|
||||
"settings-prfmMode-latency": "Low latency",
|
||||
"settings-prfmMode-smoothScreenshare": "Smooth screenshare",
|
||||
"settings-prfmMode-dynamic": "Dynamic",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Disable autogain",
|
||||
|
|
@ -138,11 +152,14 @@
|
|||
"settings-save": "Save Settings",
|
||||
"settings-experimental": "Experimental",
|
||||
"settings-restart": "Restart App",
|
||||
"invite-goBackToApp": "Go back to the Discord app",
|
||||
"settings-updater": "Check for updates",
|
||||
"settings-skipSplash": "Skip Splash Screen",
|
||||
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
|
||||
"settings-copyDebugInfo": "Copy Debug Info",
|
||||
"settings-copyGPUInfo": "Copy GPU Info",
|
||||
"settings-openWebRTCInternals": "Open WebRTC Internals",
|
||||
"settings-openGPUInfo": "Open GPU Info",
|
||||
"settings-clearClientModCache": "Clear client mod cache",
|
||||
"settings-forceNativeCrash": "Force native crash",
|
||||
"settings-smoothScroll": "Use smooth scrolling",
|
||||
|
|
@ -152,10 +169,39 @@
|
|||
"settings-quickCss": "Quick CSS",
|
||||
"settings-quickCss-desc": "Quickly edit your CSS in a simple text editor. Changes are applied immediately after saving the file.",
|
||||
"settings-category-lookAndFeel": "Look and feel",
|
||||
"settings-category-lookAndFeel-desc": "Window chrome, tray icon, splash screen, and mobile layout.",
|
||||
"settings-category-mods": "Mods",
|
||||
"settings-category-mods-desc": "Client mods, CSP, and plugin capabilities.",
|
||||
"settings-category-behaviour": "Behaviour",
|
||||
"settings-category-behaviour-desc": "Startup, tray, scrolling, spellcheck, and Discord channel.",
|
||||
"settings-category-legacy": "Legacy features",
|
||||
"settings-category-networking": "Networking",
|
||||
"settings-category-networking-desc": "HTTP(S) and SOCKS proxy settings for Discord and downloads.",
|
||||
"settings-category-debug": "Debug options",
|
||||
"settings-category-powerManagement": "Power Management",
|
||||
"settings-category-powerManagement-desc": "Performance presets and power-saving behaviour.",
|
||||
"settings-category-arrpc": "Rich Presence",
|
||||
"settings-category-arrpc-desc": "arRPC game activity detection and scanning.",
|
||||
"settings-category-backup": "Backup and restore",
|
||||
"settings-category-backup-desc": "Export or restore settings, themes, plugins, and mod data.",
|
||||
"settings-category-advanced": "Advanced",
|
||||
"settings-category-advanced-desc": "Audio capture, hardware, updates, flags, and debug tools.",
|
||||
"settings-search": "Search settings",
|
||||
"settings-search-desc": "Filter settings by name or description.",
|
||||
"settings-noResults": "No settings match your search.",
|
||||
"settings-proxyMode": "Proxy mode",
|
||||
"settings-proxyMode-desc": "Choose how Legcord connects through a proxy. System uses your OS proxy (and HTTP(S)_PROXY environment variables). Fixed servers and PAC work like in a normal web browser. A restart is required.",
|
||||
"settings-proxyMode-system": "System",
|
||||
"settings-proxyMode-direct": "Direct (no proxy)",
|
||||
"settings-proxyMode-fixed_servers": "Fixed servers",
|
||||
"settings-proxyMode-pac_script": "PAC script",
|
||||
"settings-proxyMode-auto_detect": "Auto-detect",
|
||||
"settings-proxyRules": "Proxy server",
|
||||
"settings-proxyRules-desc": "Proxy URL or Chromium proxy rules. Examples: http://127.0.0.1:8080, socks5://127.0.0.1:1080, or http=proxy:80;https=proxy:80",
|
||||
"settings-proxyPacScript": "PAC script URL",
|
||||
"settings-proxyPacScript-desc": "URL of a proxy auto-config (PAC) file, e.g. http://wpad/wpad.dat or file:///path/to/proxy.pac",
|
||||
"settings-proxyBypassRules": "Proxy bypass list",
|
||||
"settings-proxyBypassRules-desc": "Comma-separated hosts that skip the proxy. Use <local> for localhost. Example: <local>,*.intranet.example,10.0.0.0/8",
|
||||
"menu-about": "About Legcord",
|
||||
"menu-developerTools": "Developer tools",
|
||||
"menu-openSettings": "Open settings",
|
||||
|
|
@ -203,10 +249,13 @@
|
|||
"setup-welcomeTitle": "Welcome to Legcord",
|
||||
"setup-welcomeSubtitle": "Let's get you set up with your perfect configuration.",
|
||||
"setup-getStarted": "Get Started",
|
||||
"setup-windowStyle-overlayTitle": "Overlay Titlebar",
|
||||
"setup-windowStyle-overlayDesc": "A modern titlebar that blends into Discord. Recommended for most users.",
|
||||
"setup-windowStyle-nativeTitle": "Native Window",
|
||||
"setup-windowStyle-nativeDesc": "Use your system's default window decorations",
|
||||
"setup-windowStyle-customTitle": "Custom Titlebar",
|
||||
"setup-windowStyle-customDesc": "Use Legcord's custom titlebar design",
|
||||
"setup-recommended": "Recommended",
|
||||
"setup-chooseWindowStyle": "Choose Window Style",
|
||||
"setup-selectAppearance": "Select how Legcord appears on your machine",
|
||||
"setup-systemTray": "System Tray",
|
||||
|
|
@ -222,6 +271,8 @@
|
|||
"setup-launchLegcord": "Launch Legcord",
|
||||
"setup-modSelectorTitle": "Choose Your Client Mod",
|
||||
"setup-modSelectorSubtitle": "Legcord includes Shelter out of the box, but you can also choose another client mod if wanted.",
|
||||
"setup-shelterOnlyTitle": "Shelter Only",
|
||||
"setup-shelterOnlyDesc": "Legcord was built around Shelter. Most typical users won't need another client mod.",
|
||||
"setup-vencordTitle": "Vencord",
|
||||
"setup-vencordDesc": "Client mod with plugins and themes.",
|
||||
"setup-equicordTitle": "Equicord",
|
||||
|
|
@ -239,8 +290,6 @@
|
|||
"settings-channel-stable": "Stable",
|
||||
"settings-channel-canary": "Canary",
|
||||
"settings-channel-ptb": "PTB",
|
||||
"settings-category-powerManagement": "Power Management",
|
||||
"settings-category-arrpc": "arRPC",
|
||||
"settings-extendedPluginAbilities": "Extended plugin abilities",
|
||||
"settings-extendedPluginAbilities-desc": "Allows plugins to read and write files in a scoped folder on your computer (e.g. for caching deleted messages). Only enable for plugins you trust—they can store data locally. Data is stored per plugin in Legcord's plugin-storage folder.",
|
||||
"settings-audio-loopback": "Loopback",
|
||||
|
|
@ -259,6 +308,7 @@
|
|||
"keybind-navigateBack": "Navigate back",
|
||||
"keybind-runJavascript": "Run Javascript",
|
||||
"keybind-openQuickCss": "Open Quick CSS",
|
||||
"keybind-openSettings": "Open Settings",
|
||||
"keybind-globalNote": "Allows you to assign a specific keyboard shortcut that can be used across different applications and programs.",
|
||||
"keybind-global": "Global",
|
||||
"keybind-enabled": "Enabled",
|
||||
|
|
@ -271,12 +321,21 @@
|
|||
"detectable-appName": "App Name*",
|
||||
"detectable-appId": "App ID*",
|
||||
"detectable-themes": "Themes",
|
||||
"detectable-themes-note": "Optional genre tags for this game, comma-separated.",
|
||||
"detectable-aliases": "Aliases",
|
||||
"detectable-aliases-note": "Optional alternate names Discord may match, comma-separated.",
|
||||
"detectable-appId-note": "Discord application ID from the Developer Portal for this game’s rich presence.",
|
||||
"detectable-enabled": "Enabled",
|
||||
"detectable-placeholderName": "e.g. Discord",
|
||||
"detectable-placeholderId": "e.g. 1234567890",
|
||||
"detectable-placeholderThemes": "Action, Adventure",
|
||||
"detectable-placeholderAliases": "Alias1, Alias2",
|
||||
"themes-pageTitle": "Themes",
|
||||
"themes-pageDesc": "Install BetterDiscord themes or write Quick CSS to customize 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-updated": "Theme successfully updated!",
|
||||
"themes-bdInstalled": "BD theme successfully installed!",
|
||||
|
|
@ -289,22 +348,48 @@
|
|||
"themes-importFromFile": "Import from file",
|
||||
"themes-openThemesFolder": "Open themes folder",
|
||||
"themes-import": "Import",
|
||||
"themes-refresh": "Refresh",
|
||||
"themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]",
|
||||
"themes-installed": "Installed Themes",
|
||||
"keybinds-pageTitle": "Keybinds",
|
||||
"keybinds-pageDesc": "Create keyboard shortcuts for Legcord actions like mute, deafen, or opening Quick CSS. Enable Global on a keybind to use it even when Discord is in the background.",
|
||||
"keybinds-empty": "No keybinds added yet.",
|
||||
"keybinds-emptyDesc": "Start by adding one with the button above.",
|
||||
"games-registeredGames": "Registered Games",
|
||||
"games-pageDesc": "Manage custom games for Rich Presence (arRPC). Pick a running process, then set its Discord application ID. Changes usually need a restart to apply.",
|
||||
"games-refreshList": "Refresh list",
|
||||
"games-add": "Add",
|
||||
"games-remove": "Remove",
|
||||
"games-removeConfirmHeader": "Remove game?",
|
||||
"games-removeConfirmBody": "This game will be removed from rich presence. The client must be restarted for changes to take effect.",
|
||||
"games-noExecutables": "No executables",
|
||||
"games-empty": "No registered games. 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-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-blacklisted": "Blacklisted games",
|
||||
"games-blacklistedEmpty": "No blacklisted games.",
|
||||
"games-blacklistedEmptyDesc": "Games you blacklist won’t show up in last detected.",
|
||||
"games-removeFromBlacklist": "Remove",
|
||||
"games-application": "Application",
|
||||
"plugins-pageTitle": "Plugins",
|
||||
"plugins-pageDesc": "Experimental filesystem plugins loaded from your Legcord plugins folder. Each plugin can run code in the main process, preload, and/or renderer. Only enable plugins you trust.",
|
||||
"plugins-empty": "No runtime plugins found.",
|
||||
"plugins-emptyDesc": "Open the plugins folder and add a plugin directory with a manifest.json, then refresh this list.",
|
||||
"plugins-refresh": "Refresh list",
|
||||
"plugins-openFolder": "Open plugins folder",
|
||||
"plugins-reload": "Reload",
|
||||
"plugins-targets": "Targets",
|
||||
"plugins-targetsNone": "none",
|
||||
"plugins-incompatible": "Plugin is not compatible.",
|
||||
"plugins-toastTitle": "Plugins",
|
||||
"plugins-toastEnableFailed": "Failed to enable {name}.",
|
||||
"plugins-toastDisableFailed": "Failed to disable {name}.",
|
||||
"plugins-toastReloaded": "Reloaded {name}.",
|
||||
"plugins-toastReloadFailed": "Failed to reload {name}.",
|
||||
"plugins-toastIncompatible": "{name} is not compatible with this Legcord version.",
|
||||
"screenshare-selectSource": "Please select a source",
|
||||
"screenshare-venmicDisabled": "Venmic disabled",
|
||||
"screenshare-share": "Share",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"settings-transparency-universal": "Universal",
|
||||
"settings-transparency-modern": "Moderna",
|
||||
"settings-theme-transparent": "Transparente",
|
||||
"settings-transparency-tahoe-warning": "La transparencia puede provocar lag excesivo en MacOS 26 Tahoe.",
|
||||
"settings-transparency-tahoe-warning": "La transparencia puede provocar lag excesivo en macOS 26 Tahoe.",
|
||||
"settings-popoutPiP": "Ventana emergente siempre visible",
|
||||
"settings-popoutPiP-desc": "Cuanto está habilitado, la ventana emergente estará en modo siempre visible.",
|
||||
"settings-venmic-workaround": "Workaraound",
|
||||
|
|
@ -52,8 +52,8 @@
|
|||
"settings-csp-desc": "Legcord CSP es nuestro sistema que gestiona la carga de contenido personalizado en la aplicación Discord. Elementos como los mods y los temas del cliente dependen de él. Desactívalo si quieres deshacerte de los mods y los estilos personalizados.",
|
||||
"settings-mintoTray": "Trabajar en segundo plano",
|
||||
"settings-mintoTray-desc": "Cuando está desactivado, Legcord se cerrará como cualquier otra ventana al cerrarlo; de lo contrario, permanecerá en la bandeja del sistema para su uso posterior.",
|
||||
"settings-startMinimized": "Iniciar minimizado",
|
||||
"settings-startMinimized-desc": "Legcord se inicia en segundo plano y no interfiere en tu trabajo.",
|
||||
"settings-startMinimized": "Ventana al iniciar",
|
||||
"settings-startMinimized-desc": "Elige cómo aparece Legcord al iniciar. El modo bandeja oculta la ventana por completo (hace falta el icono de bandeja).",
|
||||
"settings-useSystemCssEditor": "Utilizar el editor CSS del sistema",
|
||||
"settings-useSystemCssEditor-desc": "Utilice el editor CSS del sistema para editar CSS.",
|
||||
"settings-MultiInstance": "Múltiples instancias",
|
||||
|
|
@ -95,7 +95,12 @@
|
|||
"settings-prfmMode": "Modo rendimiento",
|
||||
"settings-prfmMode-desc": "El modo de rendimiento es una función experimental de Legcord diseñada para optimizar la capacidad de respuesta y el rendimiento en función de tus necesidades. El impacto puede variar en función de tu hardware y uso, por lo que te recomendamos que pruebes cada modo para determinar cuál te conviene más.",
|
||||
"settings-prfmMode-performance": "Rendimiento",
|
||||
"settings-prfmMode-balanced": "Equilibrado",
|
||||
"settings-prfmMode-battery": "Batería",
|
||||
"settings-prfmMode-memory": "Ahorro de memoria",
|
||||
"settings-prfmMode-voip": "Voz y vídeo",
|
||||
"settings-prfmMode-latency": "Baja latencia",
|
||||
"settings-prfmMode-smoothScreenshare": "Compartir pantalla fluido",
|
||||
"settings-prfmMode-dynamic": "Dinámico",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Desactivar ganancia automática",
|
||||
|
|
@ -298,5 +303,9 @@
|
|||
"games-removeFromBlacklist": "Eliminar",
|
||||
"games-application": "Aplicación",
|
||||
"settings-vaapi": "VAAPI",
|
||||
"settings-vaapi-desc": "Utiliza VAAPI (aceleración por hardware) para la decodificación de vídeo en Linux. Esto reduce considerablemente el uso de la CPU durante el uso compartido de pantalla, pero puede causar problemas en algunos sistemas. Desactívalo si experimentas bloqueos o pantallas en negro durante el uso compartido de pantalla."
|
||||
"settings-vaapi-desc": "Utiliza VAAPI (aceleración por hardware) para la decodificación de vídeo en Linux. Esto reduce considerablemente el uso de la CPU durante el uso compartido de pantalla, pero puede causar problemas en algunos sistemas. Desactívalo si experimentas bloqueos o pantallas en negro durante el uso compartido de pantalla.",
|
||||
"settings-material": "Material de Ventana",
|
||||
"settings-startMinimized-off": "Normal",
|
||||
"settings-startMinimized-minimized": "Minimizada en la barra de tareas",
|
||||
"settings-startMinimized-tray": "Oculta en la bandeja"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"settings-transparency-universal": "Universel",
|
||||
"settings-transparency-modern": "Moderne",
|
||||
"settings-theme-transparent": "Transparent",
|
||||
"settings-transparency-tahoe-warning": "La transparence peut causer un lag excessif sur MacOS 26 Tahoe.",
|
||||
"settings-transparency-tahoe-warning": "La transparence peut causer un lag excessif sur macOS 26 Tahoe.",
|
||||
"settings-venmic-deviceSelect": "Sélection du périphérique",
|
||||
"settings-venmic-deviceSelect-desc": "Autoriser la sélection d'un périphérique audio.",
|
||||
"settings-venmic-granularSelect-desc": "Autoriser la sélection d'une source d'entrée audio.",
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"settings-audio-loopback": "Loopback",
|
||||
"settings-audio-loopbackWithMute": "Loopback avec sourdine",
|
||||
"settings-openCustomIconDialog": "Définir une icône de bureau",
|
||||
"settings-startMinimized-desc": "Legcord démarre en arrière-plan sans vous déranger.",
|
||||
"settings-startMinimized-desc": "Choisit comment Legcord apparaît au lancement. Le mode tray masque complètement la fenêtre (icône de tray requise).",
|
||||
"settings-useSystemCssEditor": "Utiliser l'éditeur CSS du système",
|
||||
"settings-useSystemCssEditor-desc": "Utiliser l'éditeur CSS du système pour modifier le CSS.",
|
||||
"settings-MultiInstance": "Multi-instances",
|
||||
|
|
@ -57,7 +57,12 @@
|
|||
"settings-prfmMode": "Mode performance",
|
||||
"settings-prfmMode-desc": "Le mode performance est une fonctionnalité expérimentale de Legcord conçue pour optimiser la réactivité et les performances selon vos besoins. Son impact peut varier selon votre matériel et votre utilisation, c'est pourquoi nous vous encourageons à essayer chaque mode pour déterminer lequel vous correspond le mieux.",
|
||||
"settings-prfmMode-performance": "Performance",
|
||||
"settings-prfmMode-balanced": "Équilibré",
|
||||
"settings-prfmMode-battery": "Batterie",
|
||||
"settings-prfmMode-memory": "Économie mémoire",
|
||||
"settings-prfmMode-voip": "Voix et vidéo",
|
||||
"settings-prfmMode-latency": "Faible latence",
|
||||
"settings-prfmMode-smoothScreenshare": "Partage d'écran fluide",
|
||||
"settings-prfmMode-dynamic": "Dynamique",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableHttpCache": "Désactiver le cache HTTP",
|
||||
|
|
@ -104,7 +109,7 @@
|
|||
"settings-csp-desc": "Legcord CSP est notre système qui gère le chargement du contenu personnalisé dans l'application Discord. Des éléments tels que les mods et les thèmes en dépendent. Désactivez-le si vous souhaitez vous débarrasser des mods et des styles personnalisés.",
|
||||
"settings-mintoTray": "Actif en arrière-plan",
|
||||
"settings-mintoTray-desc": "Si désactivé, Legcord se fermera comme n'importe quelle autre fenêtre, sinon il restera actif dans la barre d'état pour plus tard.",
|
||||
"settings-startMinimized": "Démarrer en mode minimisé",
|
||||
"settings-startMinimized": "Fenêtre au démarrage",
|
||||
"settings-noBundleUpdates": "Pas de mises à jour",
|
||||
"settings-noBundleUpdates-desc": "Désactive les mises à jour automatique des mods.",
|
||||
"settings-mobileMode-desc": "Si vous êtes sur un appareil doté d'un écran tactile cette fonctionnalité est faite pour vous ! Elle active le mode mobile caché de Discord conçu pour les téléphones et tablettes. La seule fonction majeure manquante est le tchat vocal. Idéal pour les utilisateurs de PinePhone ou équivalents.",
|
||||
|
|
@ -339,5 +344,8 @@
|
|||
"supportBanner-title": "Soutenir le projet",
|
||||
"supportBanner-subtitle": "Aidez-nous à continuer le développement de Legcord. Votre soutien permet de maintenir le projet en vie et nous permet de proposer plus de fonctionnalités.",
|
||||
"supportBanner-donate": "Faire un don",
|
||||
"keybind-enabled": "Activé"
|
||||
"keybind-enabled": "Activé",
|
||||
"settings-startMinimized-off": "Normale",
|
||||
"settings-startMinimized-minimized": "Réduite dans la barre des tâches",
|
||||
"settings-startMinimized-tray": "Masquée dans le tray"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"settings-theme-desc": "Az ablakstílus kezeli, hogy a Legcord milyen fejlécet használ.",
|
||||
"settings-sleepInBackground-desc": "Engedélyezi a Chromium háttérben történő lassítását. Ez segíthet az akkumulátor élettartamának megőrzésében, de egyben megszünteti az értesítéseket.",
|
||||
"settings-transparency-desc": "Állítsd be, milyen átlátszósági módot használjon a Legcord.",
|
||||
"settings-transparency-tahoe-warning": "Az átlátszóság késleltetést okozhat MacOS 26 Tahoe rendszeren.",
|
||||
"settings-transparency-tahoe-warning": "Az átlátszóság késleltetést okozhat macOS 26 Tahoe rendszeren.",
|
||||
"settings-popoutPiP": "A hívás felugró ablaka mindig felül",
|
||||
"settings-popoutPiP-desc": "Ha engedélyezve van, a hívás felugró ablaka mindig felül lesz.",
|
||||
"settings-venmic-workaround": "Ideiglenes megoldás",
|
||||
|
|
@ -48,11 +48,11 @@
|
|||
"settings-audio": "Hang",
|
||||
"settings-audio-desc": "Válassza ki, hogy a Legcord melyik módszert használja a hang rögzítéséhez a képernyőmegosztás közben.",
|
||||
"settings-openCustomIconDialog": "Asztali ikon beállítása",
|
||||
"settings-csp-desc": "A Legcord CSP kezeli az egyéni tartalmak Discord alkalmazásba való betöltését. Olyan dolgok, mint a kliens bővítmények és témák, ettől függenek. Kapcsold ki, ha meg szeretnél szabadulni a bővítményektől és az egyéni stílusoktól.",
|
||||
"settings-csp-desc": "Állítsd be a Legcord tartalombiztonsági szabályzatának szigorúságát. A szigorú CSP jobb biztonságot nyújt, de kompatibilitási problémákat okozhat egyes kliens modokkal/témákkal/bővítményekkel.",
|
||||
"settings-mintoTray": "Futás a háttérben",
|
||||
"settings-mintoTray-desc": "Ha le van tiltva, a Legcord a többi ablakhoz hasonlóan bezáródik, különben csak a tálcára csukódik le.",
|
||||
"settings-startMinimized": "Kisablakos indítás",
|
||||
"settings-startMinimized-desc": "A Legcord a háttérben indul el, és nem zavar.",
|
||||
"settings-startMinimized": "Induló ablak",
|
||||
"settings-startMinimized-desc": "Meghatározza, hogyan jelenik meg a Legcord indításkor. A tálca mód teljesen elrejti az ablakot (tálcaikon szükséges).",
|
||||
"settings-useSystemCssEditor": "Használja a rendszer CSS-szerkesztőjét",
|
||||
"settings-useSystemCssEditor-desc": "A CSS szerkesztéséhez használja a rendszer CSS-szerkesztőjét.",
|
||||
"settings-MultiInstance": "Több példány",
|
||||
|
|
@ -83,8 +83,8 @@
|
|||
"settings-bitrateTarget-desc": "Cél bitráta képernyőmegosztáshoz.",
|
||||
"settings-invitewebsocket": "Rich Presence",
|
||||
"settings-invitewebsocket-desc": "Az arRPC-t használja a Discord RPC (Rich Presence) támogatásához a gépeden futó helyi programokkal.",
|
||||
"settings-useMacSystemPicker": "MacOS rendszerválasztó használata",
|
||||
"settings-useMacSystemPicker-desc": "Amikor csak lehetséges, natív MacOS képernyőmegosztást használjon. Csak MacOS 15+ rendszeren.",
|
||||
"settings-useMacSystemPicker": "macOS rendszerválasztó használata",
|
||||
"settings-useMacSystemPicker-desc": "Amikor csak lehetséges, natív macOS képernyőmegosztást használjon. Csak macOS 15+ rendszeren.",
|
||||
"settings-additionalArguments": "További argumentumok",
|
||||
"settings-additionalArguments-desc": "A további argumentumok extra parancsok, amelyeket átadhatsz a Legcordnak. Használhatók funkciók engedélyezésére vagy letiltására, illetve problémák megoldására.",
|
||||
"settings-mod": "Kliens bővítmény",
|
||||
|
|
@ -94,7 +94,12 @@
|
|||
"settings-prfmMode": "Teljesítmény mód",
|
||||
"settings-prfmMode-desc": "A Teljesítmény mód egy kísérleti funkció a Legcordban, melynek célja a válaszidő és a teljesítmény optimalizálása az Ön igényei alapján. A hatás a hardvertől és a használattól függően változhat, ezért javasoljuk, hogy próbálja ki az egyes módokat, hogy megállapítsa, melyik működik legjobban az Ön számára.",
|
||||
"settings-prfmMode-performance": "Teljesítmény",
|
||||
"settings-prfmMode-balanced": "Kiegyensúlyozott",
|
||||
"settings-prfmMode-battery": "Akkumulátor",
|
||||
"settings-prfmMode-memory": "Memóriatakarékos",
|
||||
"settings-prfmMode-voip": "Hang és videó",
|
||||
"settings-prfmMode-latency": "Alacsony késleltetés",
|
||||
"settings-prfmMode-smoothScreenshare": "Sima képernyőmegosztás",
|
||||
"settings-prfmMode-dynamic": "Dinamikus",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Automatikus erősítés letiltása",
|
||||
|
|
@ -290,9 +295,9 @@
|
|||
"touchbar-servers": "Szerverek",
|
||||
"splash-title": "Legcord",
|
||||
"settings-venmic-granularSelect": "Részletes kiválasztás",
|
||||
"settings-trayIcon-white-plug-alt": "Fehér Plug alternatíva",
|
||||
"settings-trayIcon-black-plug": "Fekete Plug",
|
||||
"settings-trayIcon-black-plug-alt": "Fekete Plug alternatíva",
|
||||
"settings-trayIcon-white-plug-alt": "Fehér csatlakozó alternatíva",
|
||||
"settings-trayIcon-black-plug": "Fekete csatlakozó",
|
||||
"settings-trayIcon-black-plug-alt": "Fekete csatlakozó alternatíva",
|
||||
"menu-redo": "Újra",
|
||||
"setup-stepOf": "{current} / {total} lépés",
|
||||
"keybind-deafen": "Süketítés",
|
||||
|
|
@ -339,5 +344,15 @@
|
|||
"settings-material-mica": "Mica",
|
||||
"settings-material-mica-alt": "Mica Alt",
|
||||
"settings-material-acrylic": "Akril",
|
||||
"settings-material-none": "None"
|
||||
"settings-material-none": "Egyik sem",
|
||||
"settings-csp-vanilla": "Vanilla",
|
||||
"settings-csp": "Tartalombiztonsági irányelv",
|
||||
"settings-csp-strict": "Szigorú",
|
||||
"settings-csp-none": "Egyik sem",
|
||||
"keybind-openSettings": "Beállítások megnyitása",
|
||||
"settings-firstTimeCrash": "Mindent előkészítünk számodra!",
|
||||
"settings-firstTimeCrash-desc": "Az első indításkor a beállítások még nem érhetők el. Kérjük, használd az alábbi gombot az újraindításhoz, és a beállítások az újraindítás után már elérhetőek lesznek.",
|
||||
"settings-startMinimized-off": "Normál",
|
||||
"settings-startMinimized-minimized": "Kis méretű a tálcán",
|
||||
"settings-startMinimized-tray": "Elrejtve a rendszertálcán"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"settings-transparency-universal": "Universal",
|
||||
"settings-transparency-modern": "Modern",
|
||||
"settings-theme-transparent": "Transparan",
|
||||
"settings-transparency-tahoe-warning": "Transparansi dapat menyebabkan lag berlebihan pada MacOS 26 Tahoe.",
|
||||
"settings-transparency-tahoe-warning": "Transparansi dapat menyebabkan lag berlebihan pada macOS 26 Tahoe.",
|
||||
"settings-popoutPiP": "Panggilan Popout Selalu di Atas",
|
||||
"settings-popoutPiP-desc": "Saat diaktifkan, jendela popout panggilan akan berada dalam mode Selalu di Atas.",
|
||||
"settings-venmic-workaround": "Solusi sementara",
|
||||
|
|
@ -49,11 +49,11 @@
|
|||
"settings-audio": "Audio",
|
||||
"settings-audio-desc": "Pilih metode yang digunakan Legcord untuk mengambil audio dari sistem Anda selama berbagi layar.",
|
||||
"settings-openCustomIconDialog": "Atur ikon desktop",
|
||||
"settings-csp-desc": "Legcord CSP adalah sistem kami yang mengelola pemuatan konten kustom ke dalam aplikasi Discord. Fitur seperti mod klien dan tema bergantung padanya. Nonaktifkan jika Anda ingin menghapus mod dan gaya kustom.",
|
||||
"settings-csp-desc": "Atur tingkat keketatan Kebijakan Keamanan Konten Legcord. CSP ketat memberikan keamanan lebih baik tetapi dapat menyebabkan masalah kompatibilitas dengan beberapa mod/tema/plugin klien.",
|
||||
"settings-mintoTray": "Bekerja di latar belakang",
|
||||
"settings-mintoTray-desc": "Ketika dinonaktifkan, Legcord akan ditutup seperti jendela lainnya saat ditutup, sedangkan jika tidak, ia akan tetap berada di area sistem tray Anda untuk digunakan nanti.",
|
||||
"settings-startMinimized": "Mulai dalam mode minimized",
|
||||
"settings-startMinimized-desc": "Legcord berjalan di latar belakang dan tidak mengganggu Anda.",
|
||||
"settings-startMinimized": "Jendela saat mulai",
|
||||
"settings-startMinimized-desc": "Pilih bagaimana Legcord muncul saat diluncurkan. Mode tray menyembunyikan jendela sepenuhnya (ikon tray harus diaktifkan).",
|
||||
"settings-useSystemCssEditor": "Gunakan editor CSS sistem",
|
||||
"settings-useSystemCssEditor-desc": "Gunakan editor CSS sistem untuk mengedit CSS.",
|
||||
"settings-MultiInstance": "Multi Instans",
|
||||
|
|
@ -89,7 +89,12 @@
|
|||
"settings-prfmMode": "Mode kinerja",
|
||||
"settings-prfmMode-desc": "Mode Kinerja adalah fitur eksperimental di Legcord yang dirancang untuk mengoptimalkan responsivitas dan kinerja sesuai dengan kebutuhan Anda. Dampak yang dihasilkan dapat bervariasi tergantung pada spesifikasi perangkat keras dan pola penggunaan Anda, jadi kami menyarankan Anda untuk mencoba setiap mode untuk menentukan mana yang paling sesuai dengan kebutuhan Anda.",
|
||||
"settings-prfmMode-performance": "Kinerja",
|
||||
"settings-prfmMode-balanced": "Seimbang",
|
||||
"settings-prfmMode-battery": "Baterai",
|
||||
"settings-prfmMode-memory": "Penghemat memori",
|
||||
"settings-prfmMode-voip": "Suara & video",
|
||||
"settings-prfmMode-latency": "Latensi rendah",
|
||||
"settings-prfmMode-smoothScreenshare": "Berbagi layar mulus",
|
||||
"settings-prfmMode-dynamic": "Dinamis",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Nonaktifkan autogain",
|
||||
|
|
@ -286,5 +291,65 @@
|
|||
"contextMenu-searchGoogle": "Cari dengan Google",
|
||||
"contextMenu-searchDuckDuckGo": "Cari dengan DuckDuckGo",
|
||||
"touchbar-servers": "Server",
|
||||
"splash-title": "Legcord"
|
||||
"splash-title": "Legcord",
|
||||
"settings-material": "Material Jendela",
|
||||
"settings-material-desc": "Atur material latar belakang Jendela yang digunakan Legcord.",
|
||||
"settings-material-mica": "Mica",
|
||||
"settings-material-mica-alt": "Mica Alt",
|
||||
"settings-material-acrylic": "Acrylic",
|
||||
"settings-material-none": "Tidak ada",
|
||||
"settings-csp": "Kebijakan Keamanan Konten",
|
||||
"settings-csp-strict": "Ketat",
|
||||
"settings-csp-none": "Tidak ada",
|
||||
"settings-csp-vanilla": "Vanilla",
|
||||
"settings-automaticClientUpdates": "Pembaruan klien otomatis",
|
||||
"settings-automaticClientUpdates-desc": "Menonaktifkan pembaruan Legcord otomatis",
|
||||
"settings-vaapi": "VAAPI",
|
||||
"settings-vaapi-desc": "Gunakan VAAPI (akselerasi HW) untuk decoding video di Linux. Ini sangat mengurangi penggunaan CPU selama berbagi layar tetapi dapat menyebabkan masalah pada beberapa sistem. Nonaktifkan jika Anda mengalami crash atau layar hitam selama berbagi layar.",
|
||||
"settings-mod-shelter": "Shelter",
|
||||
"settings-mod-custom": "Kustom",
|
||||
"settings-quickCss": "CSS Cepat",
|
||||
"settings-quickCss-desc": "Edit CSS Anda dengan cepat di editor teks sederhana. Perubahan diterapkan segera setelah menyimpan file.",
|
||||
"keybind-pushToTalk": "Tekan untuk berbicara",
|
||||
"games-lastDetected": "Game yang terakhir terdeteksi",
|
||||
"games-lastDetectedEmpty": "Belum ada game yang terdeteksi. Mulai game dengan rich presence untuk melihatnya di sini.",
|
||||
"games-blacklist": "Daftar hitam",
|
||||
"games-blacklisted": "Game yang masuk daftar hitam",
|
||||
"games-blacklistedEmpty": "Tidak ada game yang masuk daftar hitam.",
|
||||
"games-removeFromBlacklist": "Hapus",
|
||||
"games-application": "Aplikasi",
|
||||
"backup-dialogSave-title": "Simpan cadangan Legcord",
|
||||
"backup-dialogOpen-title": "Buka cadangan Legcord",
|
||||
"backup-pageTitle": "Cadangan dan pemulihan",
|
||||
"backup-pageSubtitle": "Simpan atau pulihkan pengaturan Legcord Anda, termasuk setelan, tema, plugin, dan data mod—semua dalam satu file.",
|
||||
"backup-createBackup": "Buat cadangan",
|
||||
"backup-restore": "Pulihkan",
|
||||
"backup-modalTitle": "Pilih apa yang masuk ke dalam cadangan ini",
|
||||
"backup-includeLegcordConfig": "Setelan",
|
||||
"backup-includeLegcordThemes": "Tema dan CSS Cepat",
|
||||
"backup-includeLegcordPlugins": "Ekstensi Chrome/Penyimpanan plugin",
|
||||
"backup-includeVencord": "Data Vencord (sesi saat ini)",
|
||||
"backup-includeEquicord": "Data Equicord (sesi saat ini)",
|
||||
"backup-includeShelter": "Plugin Shelter",
|
||||
"backup-includeModBundles": "File mod klien yang diunduh (Vencord, Equicord, Shelter, bundel kustom)",
|
||||
"backup-confirmBackup": "Ekspor",
|
||||
"backup-successTitle": "Cadangan tersimpan",
|
||||
"backup-successBody": "Cadangan Anda telah berhasil disimpan.",
|
||||
"backup-cancelledTitle": "Dibatalkan",
|
||||
"backup-cancelledBody": "Tidak ada file yang disimpan.",
|
||||
"backup-failedTitle": "Cadangan gagal",
|
||||
"backup-invalidFile": "Tidak dapat membaca file cadangan tersebut.",
|
||||
"backup-unknownError": "Terjadi kesalahan.",
|
||||
"backup-restoreConfirmHeader": "Pulihkan dari cadangan?",
|
||||
"backup-restoreConfirmBody": "File di disk akan diganti di mana cadangan ini memiliki data. Anda mungkin perlu memulai ulang Legcord agar semua perubahan diterapkan.",
|
||||
"backup-restoreConfirm": "Pulihkan",
|
||||
"backup-restoreCancel": "Batal",
|
||||
"backup-restoreDoneTitle": "Pemulihan selesai",
|
||||
"backup-restoreDoneBody": "Data Anda telah dipulihkan. Mulai ulang Legcord agar semua perubahan berlaku.",
|
||||
"supportBanner-title": "Dukung Proyek",
|
||||
"supportBanner-subtitle": "Bantu kami terus mengembangkan Legcord. Dukungan Anda menjaga proyek tetap hidup dan memungkinkan kami menghadirkan lebih banyak fitur.",
|
||||
"supportBanner-donate": "Donasi",
|
||||
"settings-startMinimized-off": "Normal",
|
||||
"settings-startMinimized-minimized": "Diminimalkan ke bilah tugas",
|
||||
"settings-startMinimized-tray": "Tersembunyi di tray"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@
|
|||
"settings-transparency-desc": "Legcordの透明化モードを設定します。",
|
||||
"settings-transparency-universal": "ユニバーサル",
|
||||
"settings-transparency-modern": "モダン",
|
||||
"settings-transparency-tahoe-warning": "MacOS 26 Tahoeにおいて、透明化は過度なラグを引き起こす可能性があります。",
|
||||
"settings-transparency-tahoe-warning": "macOS 26 Tahoeにおいて、透明化は過度なラグを引き起こす可能性があります。",
|
||||
"detectable-themes": "テーマ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"settings-sleepInBackground": "Descansar em segundo plano",
|
||||
"settings-sleepInBackground-desc": "Ativa a limitação de recursos do Chromium em segundo plano. Isso pode ajudar com economia de bateria, mas pode quebrar funcionalidade das notificações.",
|
||||
"settings-transparency-modern": "Moderno",
|
||||
"settings-transparency-tahoe-warning": "Transparência pode causar perda excessiva de performance em MacOS 26 (Tahoe).",
|
||||
"settings-transparency-tahoe-warning": "Transparência pode causar perda excessiva de performance em macOS 26 (Tahoe).",
|
||||
"settings-venmic-workaround": "Solução alternativa/temporária",
|
||||
"settings-venmic-workaround-desc": "Ativa ou desativa a solução alternativa para um problema que causa o microfone ser compartilhado, invés do áudio escolhido.",
|
||||
"settings-venmic-deviceSelect": "Escolha o dispositivo",
|
||||
|
|
@ -100,8 +100,8 @@
|
|||
"settings-csp-desc": "\"Legcord CSP\" é o nosso sistema que cuida do carregamento de conteúdo customizado dentro do cliente do DIscord. Coisas como modificações e temas dependem desse sistema. Desabilite se quiser se livrar de modificações e estilos customizados.",
|
||||
"settings-mintoTray": "Funcionar em segundo plano",
|
||||
"settings-mintoTray-desc": "Quando desabilitado, Legcord será fechado como qualquer outra janela, do contrário, ele funcionará em segundo plano e ficará na bandeja do sistema disponível a qualquer momento.",
|
||||
"settings-startMinimized": "Abrir minimizado",
|
||||
"settings-startMinimized-desc": "Legcord inicia em segundo plano e fica fora do seu caminho.",
|
||||
"settings-startMinimized": "Janela ao iniciar",
|
||||
"settings-startMinimized-desc": "Escolha como o Legcord aparece ao iniciar. O modo bandeja oculta a janela por completo (é preciso o ícone da bandeja).",
|
||||
"settings-useSystemCssEditor": "Usar editor de estilo (CSS) do sistema",
|
||||
"settings-useSystemCssEditor-desc": "Usar editor de estilo padrão do sistema para editar CSS.",
|
||||
"settings-MultiInstance": "Múltiplas instâncias",
|
||||
|
|
@ -145,7 +145,12 @@
|
|||
"settings-prfmMode": "Modo de performance",
|
||||
"settings-prfmMode-desc": "Modo de performance é uma funcionalidade experimental do Legcord feita para otimizar responsitividade e performance baseado nas suas nescessidades. Impacto pode variar dependendo da configuração da sua máquina e seu uso, então recomendamos tentar cada modo e escolher um que sirva melhor para você.",
|
||||
"settings-prfmMode-performance": "Performance",
|
||||
"settings-prfmMode-balanced": "Equilibrado",
|
||||
"settings-prfmMode-battery": "Bateria",
|
||||
"settings-prfmMode-memory": "Economia de memória",
|
||||
"settings-prfmMode-voip": "Voz e vídeo",
|
||||
"settings-prfmMode-latency": "Baixa latência",
|
||||
"settings-prfmMode-smoothScreenshare": "Compartilhamento de tela suave",
|
||||
"settings-prfmMode-dynamic": "Dinâmico",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Desabilitar ganho automático",
|
||||
|
|
@ -245,5 +250,8 @@
|
|||
"setup-trayEnableDesc": "Mostrar o Legcord na bandeja do sistema",
|
||||
"setup-trayDisableTitle": "Desabilitar ícone de bandeja",
|
||||
"setup-trayDisableDesc": "Não mostrar o Legcord na bandeja do sistema",
|
||||
"setup-finishTitle": "Você está pronto!"
|
||||
"setup-finishTitle": "Você está pronto!",
|
||||
"settings-startMinimized-off": "Normal",
|
||||
"settings-startMinimized-minimized": "Minimizado na barra de tarefas",
|
||||
"settings-startMinimized-tray": "Oculto na bandeja"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"settings-transparency-universal": "Universal",
|
||||
"settings-transparency-modern": "Moderno",
|
||||
"settings-theme-transparent": "Transparente",
|
||||
"settings-transparency-tahoe-warning": "Transparência pode causar perda excessiva de performance em MacOS 26 (Tahoe).",
|
||||
"settings-transparency-tahoe-warning": "Transparência pode causar perda excessiva de performance em macOS 26 (Tahoe).",
|
||||
"settings-popoutPiP": "Elevação de Chamada sempre acima",
|
||||
"settings-venmic-workaround": "Solução alternativa/temporária",
|
||||
"settings-venmic-workaround-desc": "Ativa ou desativa a solução alternativa para um problema que causa o microfone ser partilhado, invés do áudio escolhido.",
|
||||
|
|
@ -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-prfmMode": "Modo de desempenho",
|
||||
"settings-prfmMode-performance": "Desempenho",
|
||||
"settings-prfmMode-balanced": "Equilibrado",
|
||||
"settings-prfmMode-dynamic": "Dinâmico",
|
||||
"settings-prfmMode-latency": "Baixa latência",
|
||||
"settings-prfmMode-memory": "Poupança de memória",
|
||||
"settings-prfmMode-voip": "Voz e vídeo",
|
||||
"settings-prfmMode-smoothScreenshare": "Partilha de ecrã fluida",
|
||||
"settings-none": "Nenjum",
|
||||
"settings-save": "Gravar configurações",
|
||||
"settings-restart": "Reiniciar a app",
|
||||
|
|
@ -120,5 +125,234 @@
|
|||
"contextMenu-searchGoogle": "Pesquisar com Google",
|
||||
"contextMenu-searchDuckDuckGo": "Pesquisar com DuckDuckGo",
|
||||
"touchbar-servers": "Servidores",
|
||||
"splash-title": "Legcord"
|
||||
"splash-title": "Legcord",
|
||||
"settings-bounceOnPing": "Agitar na barra de tarefas às notificações",
|
||||
"settings-bounceOnPing-desc": "Agitar a app na barra de tarefas quando receber uma notificação.",
|
||||
"settings-material-none": "Nenhum",
|
||||
"settings-popoutPiP-desc": "Quando ativado, a elevação da chamada atual sempre ficará acima de outros elementos.",
|
||||
"settings-venmic-ignoreInputMedia": "Ignorar média de entrada",
|
||||
"settings-venmic-onlyDefaultSpeakers-desc": "Usar apenas alto-falantes para seleção de áudio.",
|
||||
"settings-openCustomIconDialog": "Definir ícone de desktop",
|
||||
"settings-mintoTray": "Funcionar em segundo plano",
|
||||
"settings-mintoTray-desc": "Quando desativado, Legcord será fechado como qualquer outra janela, do contrário, ele funcionará em segundo plano e ficará na bandeja do sistema disponível a qualquer momento.",
|
||||
"settings-startMinimized": "Janela ao iniciar",
|
||||
"settings-startMinimized-desc": "Escolha como o Legcord aparece ao iniciar. O modo tabuleiro oculta a janela por completo (é preciso o ícone do tabuleiro).",
|
||||
"settings-csp-none": "Nenhum",
|
||||
"settings-useSystemCssEditor": "Usar editor de estilo (CSS) do sistema",
|
||||
"settings-useSystemCssEditor-desc": "Usar editor de estilo padrão do sistema para editar CSS.",
|
||||
"settings-MultiInstance": "Múltiplas instâncias",
|
||||
"settings-MultiInstance-desc": "Quando ativado, poderá iniciar quantas instâncias do Legcord que quiser, ao mesmo tempo.",
|
||||
"settings-noBundleUpdates": "Sem updates em pacotes",
|
||||
"settings-noBundleUpdates-desc": "Desativa updates automáticos para mods deste cliente.",
|
||||
"settings-hardwareAcceleration": "Aceleração de Hardware",
|
||||
"settings-hardwareAcceleration-desc": "Aceleração de Hardware usa a sua GPU para ajudar no desempenho gráfico. Se experienciar problemas ou bugs visuais, tente desativar esta opção.",
|
||||
"settings-processScanning": "Escaneamento de processos ativos",
|
||||
"settings-processScanning-desc": "Ativa o escaneamento de processos para encontrar jogos que executam para melhorar a detecção do RichPresence.",
|
||||
"settings-windowsLegacyScanning": "Escaneamento antigo do Windows",
|
||||
"settings-windowsLegacyScanning-desc": "Usa o escaneamento de processos antigo do Windows (prév. v1.1.6). Pode ajudar a compatibilidade de alguns sistemas mas esse método é menos eficiente.",
|
||||
"settings-scanInterval": "Intervalo de escaneamento (ms)",
|
||||
"settings-scanInterval-desc": "Configura o quão frequente (em milisegundos) o escaneamento de processos ocorre. Valores menores podem melhorar a rapidez da detecção de processos, mas vão aumentar o uso do processador.",
|
||||
"settings-blockPowerSavingInVoiceChat": "Impedir economia de pilha em chats de voz",
|
||||
"settings-blockPowerSavingInVoiceChat-desc": "Impede Legcord de ser suspenso. Mantém o sistema ativo mas permite que o ecrã seja desligado/descansado.",
|
||||
"settings-mobileMode": "Modo de telemóvel",
|
||||
"settings-mobileMode-desc": "Se está num dispositivo sensível ao toque, esta função é para si! Ativa a opção oculta do Discord para dispositivos móveis como tablets e telemóveis. Única função importante que falta é compatibilidade com chat de voz. Esta opção é ideal para\n utilizadores de PinePhone e outros.",
|
||||
"settings-spellcheck": "Corretor automático",
|
||||
"settings-spellcheck-desc": "Ajuda a corrigir palavras escritas incorretamente sublinhando-as.",
|
||||
"settings-vaapi": "VAAPI",
|
||||
"settings-vaapi-desc": "Use VAAPI (aceleração de hardware) para fazer decodificação de vídeo no Linux. Esta opção ajuda a reduzir drásticamente o uso de CPU durante o compartilhamento de ecrã, mas pode causar problemas em alguns sitemas. Desative esta opção caso expirencie travamentos ou \"ecrã-preto\" enquanto partilhar o ecrã.",
|
||||
"settings-channel": "Canal do Discord",
|
||||
"settings-channel-desc": "Ative esta configuração para mudar a instância atual do Discord que o Legcord está a usar.",
|
||||
"settings-bitrateMin": "Birate mínimo",
|
||||
"settings-bitrateMin-desc": "Bitrate mínimo para compartilhamento do ecrã.",
|
||||
"settings-bitrateMax": "Bitrate máximo",
|
||||
"settings-bitrateMax-desc": "Bitrate máximo para compartilhamento do ecrã.",
|
||||
"settings-bitrateTarget": "Bitrate Alvo",
|
||||
"settings-bitrateTarget-desc": "Bitrate alvo para compartilhamento do ecrã.",
|
||||
"settings-invitewebsocket": "Rich Presence",
|
||||
"settings-invitewebsocket-desc": "Usa arRPC para a função da Rich Presence do Discord com programas locais da sua máquina.",
|
||||
"settings-useMacSystemPicker": "Usar o seletor de sistema padrão do macOS",
|
||||
"settings-useMacSystemPicker-desc": "Usa o partilhamento de ecrã nativo do macOS quando possível. Apenas para macOS 15+",
|
||||
"settings-additionalArguments": "Argumentos adicionais",
|
||||
"settings-additionalArguments-desc": "Argumentos adicionais são comandos extras que pode adicionar ao Legcord. Eles podem ser usados para ativar ou desativar funções ou corrigir problemas.",
|
||||
"settings-mod": "Modificação de Cliente",
|
||||
"settings-mod-desc1": "Mods do cliente são programas que permitem customizar a sua experiência no Discord. Mods podem mudar a aparência do cliente, modificar comportamentos e até adicionar novas funcionalidades!",
|
||||
"settings-mod-vencord": "Mod leve e fácil de usar. Vem com uma loja integrada para plugins.",
|
||||
"settings-mod-equicord": "Nascido de uma fork de contribuidores do Vencord, traz um cliente rico em plugins.",
|
||||
"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 o seu uso, então recomendamos tentar cada modo e escolher um que sirva melhor para si.",
|
||||
"settings-prfmMode-battery": "Pilha",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Desativar ganho automático",
|
||||
"settings-disableAutogain-desc": "Desativa ganho automático.",
|
||||
"settings-disableHttpCache": "Desativar cache HTTP",
|
||||
"settings-disableHttpCache-desc": "Desativa o cache HTTP do Chromium. Desative esta opção se mods não carregarem.",
|
||||
"settings-trayIcon": "Ícone de bandeja",
|
||||
"settings-trayIcon-desc": "Escolha um ícone de bandeja.",
|
||||
"settings-trayIcon-disabled": "Desativar a bandeja",
|
||||
"settings-trayIcon-dynamic": "Dinâmico",
|
||||
"settings-trayIcon-normal": "Ícone do Discord",
|
||||
"settings-trayIcon-classic": "Ícone clássico do Discord",
|
||||
"settings-trayIcon-colored-plug": "Tomada colorida",
|
||||
"settings-trayIcon-white-plug": "Tomada branca",
|
||||
"settings-trayIcon-white-plug-alt": "Tomada branca (alternativa)",
|
||||
"settings-trayIcon-black-plug": "Tomada preta",
|
||||
"settings-trayIcon-black-plug-alt": "Tomada preta (alternativa)",
|
||||
"settings-advanced": "Zona para utilizadores avançados",
|
||||
"settings-pluginsFolder": "Abrir pasta de plugins",
|
||||
"settings-crashesFolder": "Abrir pasta de crashes nativa",
|
||||
"settings-themesFolder": "Abrir pasta de temas",
|
||||
"settings-storageFolder": "Abrir pasta de armazenamento",
|
||||
"settings-updater": "Verificar por updates",
|
||||
"settings-skipSplash": "Pular ecrã de introdução",
|
||||
"settings-skipSplash-desc": "Pula a ecrã de introdução do Legcord quando você o inicia.",
|
||||
"settings-copyDebugInfo": "Copiar informação de debug",
|
||||
"settings-copyGPUInfo": "Copiar informação de GPU",
|
||||
"settings-clearClientModCache": "Limpar o cache de mods do cliente",
|
||||
"settings-forceNativeCrash": "Forçar crash nativo",
|
||||
"settings-smoothScroll": "Usar rolamento suave",
|
||||
"settings-smoothScroll-desc": "Ativa o rolamento suave",
|
||||
"settings-autoScroll": "Ativar rolamento automático",
|
||||
"settings-autoScroll-desc": "Ativa o rolamento automático com o clique do meio do rato (Obs. O seu ambiente desktop ainda pode controlar esta operação com outra ação)",
|
||||
"settings-quickCss": "CSS Rápido",
|
||||
"settings-quickCss-desc": "Rapidamente edite o CSS do seu cliente com um editor de texto simples. Mudanças são aplicadas imediatamente depois de gravar o ficheiro.",
|
||||
"settings-category-mods": "Mods",
|
||||
"settings-category-behaviour": "Comportamento",
|
||||
"settings-category-debug": "Opções de debug",
|
||||
"menu-about": "Sobre o Legcord",
|
||||
"menu-developerTools": "Ferramentas de programador",
|
||||
"menu-paste": "Colar",
|
||||
"menu-selectAll": "Selecionar tudo",
|
||||
"menu-toggleFullscreen": "Ativar/desativar modo de ecrã cheio",
|
||||
"menu-zoomIn": "Aumentar zoom",
|
||||
"menu-zoomOut": "Diminuir zoom",
|
||||
"menu-resetZoom": "Resetar zoom",
|
||||
"menu-keybind": "Atalhos",
|
||||
"menu-legcord": "Legcord",
|
||||
"tray-openLegcord": "Abrir Legcord",
|
||||
"tray-openSettings": "Abrir Configurações",
|
||||
"tray-quitLegcord": "Fechar Legcord",
|
||||
"tray-tooltip": "Legcord",
|
||||
"dialog-openUrl-title": "Quer abrir esta ligação?",
|
||||
"dialog-openUrl-message": "Deseja abrir {url}?",
|
||||
"dialog-openUrl-detail": "Detetamos que este url não usa protocolos normais ded navegador. Isto pode significar que este url leva à um programa local no seu computador. Por favor, verifique se o reconhece antes de prosseguir!",
|
||||
"dialog-openUrl-checkbox": "Lembrar a minha resposta e ignorar este aviso em sessões futuras",
|
||||
"dialog-openUrl-no": "Não, não quero",
|
||||
"title-unreadMessages": "Tem mensagens não lidas.",
|
||||
"title-legcordSuffix": " - Legcord",
|
||||
"dialog-importTheme-title": "Selecione um tema que deseja importar",
|
||||
"dialog-importTheme-discordStyles": "Estilos do Discord",
|
||||
"dialog-customIcon-filters": "Ícones",
|
||||
"config-corrupted-title": "Opa, algo deu errado.",
|
||||
"config-corrupted-message": "Legcord detetou que o seu ficheiro de configuração está corrompido, por favor, reinicie a app e ponha novamente as suas configurações, caso o erro persista, reporte no nosso servidor do Discord ou na guia de Problemas no Github.",
|
||||
"setup-welcomeTitle": "Bem-vindo ao Legcord",
|
||||
"setup-welcomeSubtitle": "Vamos nos preparar para a sua configuração perfeita.",
|
||||
"setup-getStarted": "Começar",
|
||||
"setup-windowStyle-nativeTitle": "Janela Nativa",
|
||||
"setup-windowStyle-nativeDesc": "Usar o estilo de janela padrão do sistema",
|
||||
"setup-windowStyle-customTitle": "Barra de título customizada",
|
||||
"setup-windowStyle-customDesc": "Usa o design customizado do Legcord para a barra de título",
|
||||
"setup-chooseWindowStyle": "Escolher estilo da Janela",
|
||||
"setup-selectAppearance": "Escolher como o Legcord aparece na sua máquina",
|
||||
"setup-systemTray": "Bandeja do sistema",
|
||||
"setup-trayChoose": "Escolha se deseja ativar ou não a ícone na bandeja do sistema",
|
||||
"setup-trayEnableTitle": "Ativar ícone de bandeja",
|
||||
"setup-trayEnableDesc": "Mostrar o Legcord na bandeja do sistema",
|
||||
"setup-trayDisableTitle": "Desativar ícone de bandeja",
|
||||
"setup-trayDisableDesc": "Não mostrar o Legcord na bandeja do sistema",
|
||||
"setup-finishTitle": "Está pronto!",
|
||||
"setup-back": "Voltar",
|
||||
"settings-restartRequired": "Reinicialização necessária",
|
||||
"settings-theme-legacy": "Legado",
|
||||
"settings-channel-stable": "Estável",
|
||||
"keybind-mute": "Silenciar Microfone",
|
||||
"keybind-runJavascript": "Executar Javascript",
|
||||
"keybind-global": "Global",
|
||||
"detectable-aliases": "Apelidos",
|
||||
"backup-createBackup": "Criar backup",
|
||||
"backup-restore": "Restaurar",
|
||||
"backup-includeLegcordConfig": "Configurações",
|
||||
"backup-confirmBackup": "Exportar",
|
||||
"backup-cancelledTitle": "Cancelado",
|
||||
"backup-unknownError": "Algo deu errado.",
|
||||
"backup-restoreConfirm": "Restaurar",
|
||||
"backup-restoreCancel": "Cancelar",
|
||||
"supportBanner-donate": "Doar",
|
||||
"settings-material": "Material de Janela",
|
||||
"settings-material-desc": "Define o material de Janela de plano de fundo que o Legcord usa.",
|
||||
"settings-material-mica": "Mica",
|
||||
"settings-material-mica-alt": "Mica Alt",
|
||||
"settings-material-acrylic": "Acrílico",
|
||||
"settings-csp": "Política de Segurança de Conteúdo",
|
||||
"settings-csp-desc": "Define o quão estrito a Política de Segurança do Legcord é. CSP estrito fornece melhor segurança mas pode causar problemas de compatibilidade com algumas modificações/temas/plugins de cliente.",
|
||||
"settings-csp-strict": "Estrito",
|
||||
"settings-csp-vanilla": "Vanilla",
|
||||
"settings-firstTimeCrash": "Estamos a preparar tudo por si!",
|
||||
"settings-firstTimeCrash-desc": "Definições não estão disponíveis num primeiro lançamento. Por favor use o botão abaixo para reiniciar e definições devem estar prontas após reiniciar.",
|
||||
"settings-automaticClientUpdates": "Atualizações de cliente automáticas",
|
||||
"settings-automaticClientUpdates-desc": "Desativa atualizações do Legcord automáticas",
|
||||
"settings-mod-shelter": "Shelter",
|
||||
"settings-mod-custom": "Personalizado",
|
||||
"settings-experimental": "Experimental",
|
||||
"settings-category-lookAndFeel": "Aparência e sensação",
|
||||
"settings-category-legacy": "Funcionalidades antigas",
|
||||
"tray-supportServer": "Suportar Servidor Discord",
|
||||
"setup-linuxTrayWarning": "A funcionalidade da barra de tarefas do sistema podem ter problemas ou comportarem-se diferentemente em sistemas Linux.",
|
||||
"setup-finishSubtitle": "A sua configuração Legcord está completa e personalizada de acordo com as suas preferências.",
|
||||
"setup-finishSettingsNote": "Precisa de fazer alterações mais tarde? Encontrará todas estas opções no menu de definições do Discord abaixo do Legcord.",
|
||||
"setup-launchLegcord": "Lançar Legcord",
|
||||
"setup-modSelectorTitle": "Escolher o Seu Mod de Cliente",
|
||||
"setup-modSelectorSubtitle": "O Legcord inclui Shelter como predefinição, mas também pode escolher outro mod de cliente se preferir.",
|
||||
"setup-vencordTitle": "Vencord",
|
||||
"setup-vencordDesc": "Mod de cliente com plugins e temas.",
|
||||
"setup-equicordTitle": "Equicord",
|
||||
"setup-equicordDesc": "Um fork do Vencord com mais plugins.",
|
||||
"setup-useShelterOnly": "Usar Apenas Shelter",
|
||||
"setup-stepOf": "Passo {current} de {total}",
|
||||
"setup-windowTitle": "Configuração do Legcord",
|
||||
"settings-restartRequiredBody": "Pode precisar de reiniciar para aplicar estas mudanças.",
|
||||
"settings-restartLater": "Faço isso mais tarde",
|
||||
"settings-channel-canary": "Canary",
|
||||
"settings-channel-ptb": "PTB",
|
||||
"settings-category-powerManagement": "Gestão de Energia",
|
||||
"settings-category-arrpc": "arRPC",
|
||||
"settings-extendedPluginAbilities": "Habilidades de plugin estendidas",
|
||||
"settings-extendedPluginAbilities-desc": "Permite aos plugins a leitura e escrita de ficheiro no escopo de uma pasta do seu computador (p.ex. para caching de mensagens eliminadas). Apenas ative isto para plugins que confia—eles podem armazenar dados localmente. Dados são armazenados por cada plugin na pasta de armazenamento de plugins do Legcord.",
|
||||
"settings-audio-loopback": "Loopback",
|
||||
"settings-audio-loopbackWithMute": "Loopback com mute",
|
||||
"keybind-addKeybind": "Adicionar um atalho de teclado",
|
||||
"keybind-accelerator": "Acelerador",
|
||||
"keybind-invalidCombo": "Esta combinação de teclas é inválida ou não é suportada.",
|
||||
"keybind-deafen": "Silenciar Áudio",
|
||||
"keybind-pushToTalk": "Carregar para falar",
|
||||
"keybind-leaveCall": "Sair da chamada",
|
||||
"keybind-navigateForward": "Navegar para a frente",
|
||||
"keybind-navigateBack": "Navegar para trás",
|
||||
"keybind-openQuickCss": "Abrir Quick CSS",
|
||||
"keybind-openSettings": "Abrir Definições",
|
||||
"keybind-globalNote": "Permite-lhe atribuir um atalho de teclado específico que pode ser usado entre diferentes aplicações e programas.",
|
||||
"detectable-addApp": "Adicionar Aplicação Detetável",
|
||||
"backup-dialogSave-title": "Guardar cópia de segurança do Legcord",
|
||||
"backup-dialogOpen-title": "Abrir cópia de segurança do Legcord",
|
||||
"backup-pageTitle": "Cópia de segurança e restaurar",
|
||||
"backup-pageSubtitle": "Guardar ou restaurar a sua cópia de segurança Legcord, inclui definições, temas, plugins, e dados de mods—tudo em um ficheiro.",
|
||||
"backup-modalTitle": "Escolher o que a cópia de segurança contém",
|
||||
"backup-includeLegcordThemes": "Temas e Quick CSS",
|
||||
"backup-includeLegcordPlugins": "Extensões Chrome/Armazenamento de Plugin",
|
||||
"backup-includeVencord": "Dados Vencord (sessão atual)",
|
||||
"backup-includeEquicord": "Dados Equicord (sessão atual)",
|
||||
"backup-includeShelter": "Plugins Shelter",
|
||||
"backup-includeModBundles": "Ficheiros de mod de cliente transferidos (Vencord, Equicord, Shelter, pacotes personalizados)",
|
||||
"backup-successTitle": "Cópia de segurança guardada",
|
||||
"backup-successBody": "A sua cópia de segurança foi guardada com sucesso.",
|
||||
"backup-cancelledBody": "Nenhum ficheiro foi guardado.",
|
||||
"backup-failedTitle": "Cópia de segurança falhada",
|
||||
"backup-invalidFile": "Não foi possível ler esse ficheiro de cópia de segurança.",
|
||||
"backup-restoreConfirmHeader": "Restaurar a partir da cópia de segurança?",
|
||||
"backup-restoreConfirmBody": "Ficheiros em disco serão substituídos onde esta cópia de segurança contém dados. Poderá ter que reiniciar o Legcord para tudo ser aplicado.",
|
||||
"backup-restoreDoneTitle": "Restauração completa",
|
||||
"backup-restoreDoneBody": "Os seus dados foram restaurados. Reinicie o Legcord para que todas as mudanças tomem efeito.",
|
||||
"supportBanner-title": "Suportar o Projeto",
|
||||
"supportBanner-subtitle": "Ajude-nos a continuar a desenvolver o Legcord. O seu suporte mantém o projeto vivo e permite-nos introduzir mais funcionalidades.",
|
||||
"settings-startMinimized-off": "Normal",
|
||||
"settings-startMinimized-minimized": "Minimizado na barra de tarefas",
|
||||
"settings-startMinimized-tray": "Oculto no tabuleiro"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,20 +15,20 @@
|
|||
"setup_question5": "Хотите использовать значок в трее?",
|
||||
"settings-theme": "Стиль окна",
|
||||
"settings-theme-desc": "Стиль окна определяет, какой заголовок использует Legcord.",
|
||||
"settings-theme-default": "По умолчанию (настраиваемая)",
|
||||
"settings-theme-native": "Нативная",
|
||||
"settings-autoHideMenuBar": "Авто-скрытие строки меню",
|
||||
"settings-autoHideMenuBar-desc": "Авто-скрытие строки меню, когда она не используется.",
|
||||
"settings-theme-default": "По умолчанию (Legcord)",
|
||||
"settings-theme-native": "Системная",
|
||||
"settings-autoHideMenuBar": "Автоскрытие строки меню",
|
||||
"settings-autoHideMenuBar-desc": "Автоскрытие строки меню, когда она не используется.",
|
||||
"settings-sleepInBackground": "Переходить в спящий режим в фоне",
|
||||
"settings-sleepInBackground-desc": "Включает ограничение скорости фоновой работы Chromium. Может снизить энергопотребление, но приводит к некорректной работе уведомлений.",
|
||||
"settings-transparency": "Прозрачность",
|
||||
"settings-transparency-desc": "Установите режим прозрачности Legcord.",
|
||||
"settings-transparency-desc": "Выберите режим прозрачности, который будет использовать Legcord.",
|
||||
"settings-transparency-universal": "Универсальный",
|
||||
"settings-transparency-modern": "Современный",
|
||||
"settings-theme-transparent": "Прозрачный",
|
||||
"settings-transparency-tahoe-warning": "Прозрачность может вызвать чрезмерные лаги на MacOS 26 Tahoe.",
|
||||
"settings-popoutPiP": "Всплывающее окно вызова всегда сверху",
|
||||
"settings-popoutPiP-desc": "При включении этой функции всплывающее окно вызова будет отображаться в режиме Всегда поверх других окон.",
|
||||
"settings-transparency-tahoe-warning": "Прозрачность может вызывать чрезмерные лаги на macOS 26 Tahoe.",
|
||||
"settings-popoutPiP": "Всплывающее окно вызова поверх всех окон",
|
||||
"settings-popoutPiP-desc": "При включении этой функции всплывающее окно вызова всегда будет отображаться в режиме Поверх всех окон.",
|
||||
"settings-venmic-deviceSelect": "Выбор устройства",
|
||||
"settings-venmic-deviceSelect-desc": "Позволяет выбрать аудио устройство.",
|
||||
"settings-venmic-granularSelect-desc": "Позволить выбрать источник аудиовхода.",
|
||||
|
|
@ -39,5 +39,190 @@
|
|||
"settings-venmic-ignoreInputMedia-desc": "Игнорировать аудио вход из медиаисточников.",
|
||||
"settings-venmic-onlySpeakers": "Только динамики",
|
||||
"settings-venmic-onlySpeakers-desc": "Использовать только динамики для выбора аудиовыхода.",
|
||||
"settings-audio": "Аудио"
|
||||
"settings-audio": "Аудио",
|
||||
"settings-theme-overlay": "Оверлей",
|
||||
"settings-material": "Материал фона окна",
|
||||
"settings-material-desc": "Выберите эффект прозрачности Windows, который будет использовать Legcord.",
|
||||
"settings-material-mica": "Mica",
|
||||
"settings-material-mica-alt": "Mica Alt",
|
||||
"settings-material-acrylic": "Acrylic",
|
||||
"settings-material-none": "Не использовать",
|
||||
"settings-venmic-workaround": "Обход",
|
||||
"settings-audio-desc": "Выберите метод захвата системного аудио во время демонстрации экрана.",
|
||||
"settings-openCustomIconDialog": "Выберите иконку рабочего стола",
|
||||
"settings-mintoTray": "Работа в фоне",
|
||||
"settings-mintoTray-desc": "При выключенной настройке, закрытие окна будет полностью закрывать Legcord, а при включённой, Legcord просто пойдёт отдыхать в ваш системный трей.",
|
||||
"settings-startMinimized": "Окно при запуске",
|
||||
"settings-startMinimized-desc": "Как Legcord появляется при запуске. Режим трея полностью скрывает окно (нужна иконка в трее).",
|
||||
"settings-csp": "Политика Защиты Данных (CSP)",
|
||||
"settings-csp-desc": "Установите строгость защиты данных в Legcord. Строгий режим CSP безопаснее, но может быть несовместим с некоторыми модами/темами/плагинами.",
|
||||
"settings-csp-strict": "Строгий",
|
||||
"settings-csp-none": "Не использовать",
|
||||
"settings-csp-vanilla": "Стандартный",
|
||||
"settings-useSystemCssEditor": "Использовать системный редактор CSS",
|
||||
"settings-useSystemCssEditor-desc": "Использовать системный редактор CSS для редактирования CSS в Legcord.",
|
||||
"settings-MultiInstance": "Много сессий",
|
||||
"settings-MultiInstance-desc": "При включённой настройке вы сможете начать несколько сессий Legcord одновременно.",
|
||||
"settings-noBundleUpdates": "Не обновлять пакеты",
|
||||
"settings-noBundleUpdates-desc": "Отключает автоматическое обновление модов.",
|
||||
"settings-automaticClientUpdates": "Автообновление клиента",
|
||||
"settings-automaticClientUpdates-desc": "Отключает автоматическое обновление Legcord.",
|
||||
"settings-hardwareAcceleration": "Аппаратное ускорение",
|
||||
"settings-hardwareAcceleration-desc": "Legcord будет использовать ваш GPU для ускорения работы программы. Если вам встречаются визуальные баги, попробуйте выключить эту функцию.",
|
||||
"settings-mobileMode": "Мобильный режим",
|
||||
"settings-mobileMode-desc": "Эта функция создана специально для тех, кто использует Legcord на устройствах с сенсорным экраном! Она активирует скрытый мобильный режим Discord, созданный для планшетов и смартфонов. Режим не поддерживает голосовые чаты. Идеально подходит для пользователей PinePhone и других подобных устройств.",
|
||||
"settings-spellcheck": "Проверка орфографии",
|
||||
"settings-spellcheck-desc": "Выделяет неправильно написанные слова.",
|
||||
"settings-vaapi": "VAAPI",
|
||||
"settings-vaapi-desc": "Использовать VAAPI (Аппаратное ускорение) для декодирования видео на Linux-системах. Значительно уменьшает нагрузку на CPU во время демонстрации экрана, но может вызывать проблемы на некоторых системах. Выключите эту настройку, если вы сталкиваетесь с вылетами или чёрным экраном во время демонстрации.",
|
||||
"settings-bitrateMin": "Минимальный битрейт",
|
||||
"settings-bitrateMin-desc": "Минимальный битрейт видео при демонстрации экрана.",
|
||||
"settings-bitrateMax": "Максимальный битрейт",
|
||||
"settings-bitrateMax-desc": "Максимальный битрейт видео при демонстрации экрана.",
|
||||
"settings-mod-shelter": "Shelter",
|
||||
"settings-prfmMode": "Режим Производительности",
|
||||
"settings-prfmMode-desc": "Режимы Производительности - экспериментальная функция Legcord, оптимизирующая производительность и скорость работы программы под ваши нужды. Эффект может зависеть от вашей системной конфигурации и паттернов использования, поэтому мы рекомендуем вам попробовать каждый режим и выбрать тот, что лучше всего подходит именно вам.",
|
||||
"settings-prfmMode-performance": "Производительный",
|
||||
"settings-prfmMode-balanced": "Сбалансированный",
|
||||
"settings-prfmMode-battery": "Энергосбережение",
|
||||
"settings-prfmMode-memory": "Экономия памяти",
|
||||
"settings-prfmMode-voip": "Голос и видео",
|
||||
"settings-prfmMode-latency": "Низкая задержка",
|
||||
"settings-prfmMode-smoothScreenshare": "Плавная демонстрация экрана",
|
||||
"settings-prfmMode-dynamic": "Динамический",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Выключить автоусиление",
|
||||
"settings-disableAutogain-desc": "Отключает динамическое усиление (гейн) микрофона.",
|
||||
"settings-trayIcon": "Иконка в трее",
|
||||
"settings-trayIcon-desc": "Установите значок, который будет отображаться в меню трея.",
|
||||
"settings-advanced": "Только для продвинутых пользователей",
|
||||
"settings-pluginsFolder": "Открыть папку с плагинами",
|
||||
"settings-themesFolder": "Открыть папку с темами",
|
||||
"settings-experimental": "Экспериментальные настройки",
|
||||
"settings-restart": "Перезапустить приложение",
|
||||
"settings-updater": "Проверить наличие обновлений",
|
||||
"settings-skipSplash": "Отключить сплеш",
|
||||
"settings-skipSplash-desc": "Пропускает экран с логотипом Legcord, появляющийся при запуске приложения.",
|
||||
"settings-copyDebugInfo": "Копировать данные для отладки",
|
||||
"settings-copyGPUInfo": "Копировать информацию о GPU",
|
||||
"settings-clearClientModCache": "Очистить кеш модов",
|
||||
"settings-forceNativeCrash": "Вызвать принудительный сбой",
|
||||
"settings-smoothScroll": "Использовать плавный скроллинг",
|
||||
"settings-smoothScroll-desc": "Включить плавный скроллинг",
|
||||
"settings-autoScroll": "Разрешить автоматическую прокрутку",
|
||||
"settings-autoScroll-desc": "Разрешить автоматическую прокрутку при нажатии средней кнопки мыши (Примечание: Эта настройка не имеет приоритета над назначениями клавиш вашей среды рабочего стола)",
|
||||
"settings-quickCss": "Quick CSS",
|
||||
"settings-quickCss-desc": "Быстро внесите изменения в ваш CSS в базовом текстовом редакторе. Изменения вступят в силу сразу полсе сохранения файла.",
|
||||
"settings-category-lookAndFeel": "Внешний вид",
|
||||
"settings-category-debug": "Отладка",
|
||||
"menu-about": "Про Legcord",
|
||||
"menu-developerTools": "Настройки разработчика",
|
||||
"menu-openSettings": "Открыть настройки",
|
||||
"menu-reload": "Перезагрузка",
|
||||
"menu-restart": "Перезапуск",
|
||||
"menu-cut": "Вырезать",
|
||||
"menu-copy": "Копировать",
|
||||
"menu-paste": "Вставить",
|
||||
"menu-selectAll": "Выбрать всё",
|
||||
"menu-toggleFullscreen": "Полноэкранный режим",
|
||||
"menu-zoomIn": "Приблизить",
|
||||
"menu-zoomOut": "Отдалить",
|
||||
"menu-resetZoom": "Сбросить приближение",
|
||||
"menu-window": "Окно",
|
||||
"menu-minimize": "Свернуть",
|
||||
"menu-close": "Закрыть",
|
||||
"menu-legcord": "Legcord",
|
||||
"tray-openLegcord": "Открыть Legcord",
|
||||
"tray-openSettings": "Открыть настройки",
|
||||
"tray-supportServer": "Сервер поддержки в Discord",
|
||||
"tray-restartLegcord": "Перезапустить Legcord",
|
||||
"tray-quitLegcord": "Закрыть Legcord",
|
||||
"tray-tooltip": "Legcord",
|
||||
"dialog-openUrl-title": "Перейти по ссылке?",
|
||||
"dialog-openUrl-message": "Перейти по адресу {url}?",
|
||||
"dialog-openUrl-detail": "Данный url использует нестандартный сетевой протокол. Скорее всего, он пытается запустить локальную программу на вашем устройстве. Прежде чем продолжить, удостоверьтесь, что узнаёте её!",
|
||||
"dialog-openUrl-checkbox": "Запомнить мой ответ и не предупреждать в дальнейшем",
|
||||
"dialog-openUrl-yes": "Продолжить",
|
||||
"dialog-openUrl-no": "Отмена",
|
||||
"title-unreadMessages": "У вас есть непрочитанные сообщения.",
|
||||
"title-legcordSuffix": " - Legcord",
|
||||
"dialog-importTheme-title": "Выберите тему, которую вы хотите импортировать",
|
||||
"dialog-importTheme-button": "Импортировать",
|
||||
"dialog-importTheme-allFiles": "Все файлы",
|
||||
"config-corrupted-title": "Упс, что-то пошло не так!",
|
||||
"setup-welcomeTitle": "Добро пожаловать в Legcord",
|
||||
"setup-windowStyle-nativeTitle": "Нативное окно",
|
||||
"setup-windowStyle-nativeDesc": "Использовать системное оформление окна",
|
||||
"setup-windowStyle-customTitle": "Кастомный заголовок",
|
||||
"setup-windowStyle-customDesc": "Использовать кастомный стиль полосы заголовка Legcord",
|
||||
"setup-chooseWindowStyle": "Выберите оформление окна",
|
||||
"setup-selectAppearance": "Настройте, как Legcord будет выглядеть на вашем устройстве",
|
||||
"setup-finishTitle": "Готово!",
|
||||
"setup-finishSettingsNote": "Хотите что-то поменять? Все эти настройки всегда доступны в настройках Discord в разделе Legcord.",
|
||||
"setup-launchLegcord": "Запустить Legcord",
|
||||
"setup-vencordTitle": "Vencord",
|
||||
"setup-equicordTitle": "Equicord",
|
||||
"setup-loading": "Загрузка...",
|
||||
"settings-restartRequired": "Необходим перезапуск",
|
||||
"settings-restartRequiredBody": "Перезапустите Legcord, чтобы применить внесённые изменения.",
|
||||
"settings-restartLater": "Перезапущу позже",
|
||||
"settings-channel-stable": "Stable",
|
||||
"settings-channel-canary": "Canary",
|
||||
"settings-channel-ptb": "PTB",
|
||||
"settings-category-powerManagement": "Энергосбережение",
|
||||
"settings-category-arrpc": "arRPC",
|
||||
"settings-extendedPluginAbilities": "Расширенные возможности плагинов",
|
||||
"settings-extendedPluginAbilities-desc": "Даёт плагинам доступ к чтению и записи файлов в выделенной папке на вашем устройстве (например, для сохранения удалённых сообщений). Не включайте для плагинов, которым не доверяете, и не хотите, чтобы они сохраняли данные на вашем усторйстве. Данные будут сохраняться отдельно для каждого плагина в папке плагинов в файлах Legcord.",
|
||||
"keybind-invalidCombo": "Данное сочетание клавиш некорректно или не поддерживается.",
|
||||
"keybind-recording": "Запись",
|
||||
"keybind-record": "Начать запись",
|
||||
"keybind-action": "Действие",
|
||||
"keybind-leaveCall": "Покинуть звонок",
|
||||
"keybind-add": "Добавить",
|
||||
"keybind-delete": "Удалить",
|
||||
"detectable-missingFields": "Пропущено поле",
|
||||
"detectable-fillAllFields": "Пожалуйста, заполните все поля, чтобы продолжить.",
|
||||
"detectable-themes": "Темы",
|
||||
"detectable-placeholderName": "например, Discord",
|
||||
"detectable-placeholderId": "например, 1234567890",
|
||||
"themes-updated": "Тема успешно обновлена!",
|
||||
"themes-importFromFile": "Импортировать из файла",
|
||||
"themes-openThemesFolder": "Открыть папку с темами",
|
||||
"themes-import": "Импортировать",
|
||||
"themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]",
|
||||
"screenshare-share": "Поделиться",
|
||||
"screenshare-title": "Демонстрация экрана",
|
||||
"contextMenu-searchGoogle": "Поиск в Google",
|
||||
"contextMenu-searchDuckDuckGo": "Поиск в DuckDuckGo",
|
||||
"touchbar-servers": "Сервера",
|
||||
"splash-title": "Legcord",
|
||||
"backup-dialogSave-title": "Сохранить резервную копию Legcord",
|
||||
"backup-dialogOpen-title": "Открыть резерную копию Legcord",
|
||||
"backup-pageTitle": "Резервное копирование",
|
||||
"backup-pageSubtitle": "Сохранение и восстановление ваших настроек Legcord, включая плагины и настройки модов — всё одним файлом.",
|
||||
"backup-createBackup": "Создать резервную копию",
|
||||
"backup-restore": "Восстановить",
|
||||
"backup-modalTitle": "Выберите, что хотите сохранить",
|
||||
"backup-includeLegcordConfig": "Настройки",
|
||||
"backup-includeLegcordThemes": "Темы и Quick CSS",
|
||||
"backup-includeVencord": "Данные Vencord (текущая сессия)",
|
||||
"backup-includeEquicord": "Данные Equicord (текущая сессия)",
|
||||
"backup-includeShelter": "Плагины Shelter",
|
||||
"backup-unknownError": "Что-то пошло не так.",
|
||||
"backup-restoreCancel": "Отмена",
|
||||
"supportBanner-title": "Поддержать Проект",
|
||||
"supportBanner-subtitle": "Ваша поддержка позволяет нам продолжать разработку Legcord и вводить всё больше и больше новых функций!",
|
||||
"supportBanner-donate": "Задонатить",
|
||||
"settings-trayIcon-normal": "Лого Discord",
|
||||
"settings-trayIcon-classic": "Классическое лого Discord",
|
||||
"settings-trayIcon-colored-plug": "Цветной Штекер",
|
||||
"settings-trayIcon-white-plug": "Белый Штекер",
|
||||
"settings-trayIcon-white-plug-alt": "Белый Штекер (Альт.)",
|
||||
"settings-trayIcon-black-plug": "Чёрный Штекер",
|
||||
"settings-trayIcon-black-plug-alt": "Чёрный Штекер (Альт.)",
|
||||
"settings-category-behaviour": "Поведение",
|
||||
"menu-quit": "Выход",
|
||||
"settings-startMinimized-off": "Обычный",
|
||||
"settings-startMinimized-minimized": "Свёрнуто в панель задач",
|
||||
"settings-startMinimized-tray": "Скрыто в трее"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,8 @@
|
|||
{}
|
||||
{
|
||||
"loading_screen_start": "Legcord başlatılıyor…",
|
||||
"loading_screen_update": "Legcord’un yeni bir versiyonu var. Lütfen son versiyona güncelleyin.",
|
||||
"setup_question1": "Legcord kurulumuna hoş geldiniz",
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
"next": "Sonraki"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"settings-transparency-universal": "Універсальний",
|
||||
"settings-transparency-modern": "Сучасний",
|
||||
"settings-theme-transparent": "Прозорий",
|
||||
"settings-transparency-tahoe-warning": "Прозорість може призвести до надмірного лагу на MacOS 26 Tahoe.",
|
||||
"settings-transparency-tahoe-warning": "Прозорість може призвести до надмірного лагу на macOS 26 Tahoe.",
|
||||
"settings-popoutPiP": "Виклик спливаючого вікна завжди зверху",
|
||||
"settings-popoutPiP-desc": "Коли цю функцію ввімкнено, спливаюче вікно виклику буде в режимі «Завжди зверху».",
|
||||
"settings-venmic-workaround": "Тимчасове вирішення",
|
||||
|
|
@ -52,8 +52,8 @@
|
|||
"settings-csp-desc": "Legcord CSP – це наша система, яка керує завантаженням користувацького контенту в додаток Discord. Від неї залежать такі речі, як клієнтські моди та теми. Вимкніть, якщо хочете позбутися модів та користувацьких стилів.",
|
||||
"settings-mintoTray": "Робота у фоновому режимі",
|
||||
"settings-mintoTray-desc": "Якщо вимкнено, Legcord закриється, як і будь-яке інше вікно, якщо його закрити, інакше він залишиться в системному треї на потім.",
|
||||
"settings-startMinimized": "Почати згорнутий",
|
||||
"settings-startMinimized-desc": "Легкорд починається на задньому плані та залишається поза вашими перешкодами.",
|
||||
"settings-startMinimized": "Вікно при запуску",
|
||||
"settings-startMinimized-desc": "Як Legcord з’являється під час запуску. Режим трея повністю ховає вікно (потрібна іконка в треї).",
|
||||
"settings-useSystemCssEditor": "Використовуйте системний редактор CSS",
|
||||
"settings-useSystemCssEditor-desc": "Використовуйте системний редактор CSS для редагування CSS.",
|
||||
"settings-MultiInstance": "Багато екземплярів",
|
||||
|
|
@ -89,7 +89,12 @@
|
|||
"settings-prfmMode": "Режим продуктивності",
|
||||
"settings-prfmMode-desc": "Режим продуктивності – це експериментальна функція в Legcord, розроблена для оптимізації швидкості реагування та продуктивності відповідно до ваших потреб. Вплив може відрізнятися залежно від вашого обладнання та використання, тому ми рекомендуємо вам спробувати кожен режим, щоб визначити, який найкраще підходить саме вам.",
|
||||
"settings-prfmMode-performance": "Продуктивність",
|
||||
"settings-prfmMode-balanced": "Збалансований",
|
||||
"settings-prfmMode-battery": "Батарея",
|
||||
"settings-prfmMode-memory": "Економія пам'яті",
|
||||
"settings-prfmMode-voip": "Голос і відео",
|
||||
"settings-prfmMode-latency": "Низька затримка",
|
||||
"settings-prfmMode-smoothScreenshare": "Плавна демонстрація екрана",
|
||||
"settings-prfmMode-dynamic": "Динамічний",
|
||||
"settings-prfmMode-vaapi": "VAAPI",
|
||||
"settings-disableAutogain": "Вимкнути автоматичне посилення",
|
||||
|
|
@ -137,5 +142,8 @@
|
|||
"settings-windowsLegacyScanning": "Сканування застарілих версій Windows",
|
||||
"settings-windowsLegacyScanning-desc": "Використовує застарілий метод для процесів сканування у Windows (до версії 1.1.6). Може покращити сумісність у деяких системах, але менш ефективний.",
|
||||
"settings-scanInterval": "Інтервал сканування (мс)",
|
||||
"settings-scanInterval-desc": "Встановлює частоту (у мілісекундах) сканування процесу. Нижчі значення можуть покращити швидкість виявлення, але можуть збільшити використання процесора."
|
||||
"settings-scanInterval-desc": "Встановлює частоту (у мілісекундах) сканування процесу. Нижчі значення можуть покращити швидкість виявлення, але можуть збільшити використання процесора.",
|
||||
"settings-startMinimized-off": "Звичайний",
|
||||
"settings-startMinimized-minimized": "Згорнуто на панель завдань",
|
||||
"settings-startMinimized-tray": "Сховано в треї"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@
|
|||
"settings-transparency-universal": "通用",
|
||||
"settings-transparency-modern": "现代",
|
||||
"settings-theme-transparent": "透明",
|
||||
"settings-transparency-tahoe-warning": "在 MacOS 26 Tahoe 上,启用透明效果可能会导致明显卡顿。",
|
||||
"settings-transparency-tahoe-warning": "在 macOS 26 Tahoe 上,启用透明效果可能会导致明显卡顿。",
|
||||
"settings-popoutPiP": "通话窗口弹出时保持置顶"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.5/schema.json",
|
||||
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["assets/app/js"]
|
||||
"includes": ["**", "!**/assets/app/js", "!**/assets/**/*.svg"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
|
@ -16,11 +16,11 @@
|
|||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"preset": "recommended",
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
},
|
||||
|
|
|
|||
21
docker/Dockerfile.deb
Normal file
21
docker/Dockerfile.deb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM ubuntu
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./dist/*.deb .
|
||||
|
||||
RUN apt-get update -y && apt-get upgrade -y
|
||||
|
||||
RUN apt-get install ./*.deb -y
|
||||
|
||||
RUN apt-get install -qqy x11-apps x11vnc xvfb
|
||||
|
||||
RUN mkdir ~/.vnc
|
||||
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
|
||||
|
||||
COPY docker/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 5900
|
||||
|
||||
CMD ["/start.sh"]
|
||||
21
docker/Dockerfile.pacman
Normal file
21
docker/Dockerfile.pacman
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM archlinux
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./dist/*.pacman .
|
||||
|
||||
RUN pacman -Syu --noconfirm
|
||||
|
||||
RUN pacman -U --noconfirm ./*.pacman
|
||||
|
||||
RUN pacman -Syu --noconfirm x11vnc xorg-server-xvfb
|
||||
|
||||
RUN mkdir ~/.vnc
|
||||
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
|
||||
|
||||
COPY docker/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 5900
|
||||
|
||||
CMD ["/start.sh"]
|
||||
13
docker/start.sh
Normal file
13
docker/start.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export DISPLAY=:99
|
||||
|
||||
Xvfb :99 -screen 0 1280x800x24 &
|
||||
sleep 1
|
||||
|
||||
x11vnc -display :99 -forever -usepw -rfbport 5900 &
|
||||
|
||||
legcord --no-sandbox &
|
||||
|
||||
tail -f /dev/null
|
||||
82
docs/examples/clipboard-fallback-plugin/README.md
Normal file
82
docs/examples/clipboard-fallback-plugin/README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Legcord Clipboard Fallback Plugin
|
||||
|
||||
Fixes Discord in-page copy actions in Legcord, including:
|
||||
|
||||
- Copy User ID
|
||||
- Copy Message ID
|
||||
- Copy Message Link
|
||||
- Other Discord menu actions that call `navigator.clipboard.writeText(...)`
|
||||
|
||||
## Why this exists
|
||||
|
||||
In affected Legcord/Electron environments, Discord's web UI calls:
|
||||
|
||||
```js
|
||||
navigator.clipboard.writeText(text)
|
||||
```
|
||||
|
||||
but Chromium rejects it, commonly with errors like:
|
||||
|
||||
```text
|
||||
NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Document is not focused.
|
||||
```
|
||||
|
||||
or Legcord logs:
|
||||
|
||||
```text
|
||||
Unable to determine render window for element [object HTMLDocument]
|
||||
```
|
||||
|
||||
This plugin patches `navigator.clipboard.writeText` in the Discord page and falls back to a selection-based `document.execCommand("copy")` copy path.
|
||||
|
||||
## Known limitation
|
||||
|
||||
Legcord's native **Copy Image** context-menu action does not go through `navigator.clipboard.writeText` or `navigator.clipboard.write` in the page. It is handled by Electron's main-process context menu (`webContents.copyImageAt(...)`), so a renderer/custom-bundle plugin cannot reliably fix image copying. That needs a Legcord main-process fix or a filesystem plugin with main/preload access on newer Legcord versions.
|
||||
|
||||
## Install on Legcord versions with filesystem plugins
|
||||
|
||||
1. Open the Legcord plugins folder:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/legcord/plugins
|
||||
```
|
||||
|
||||
2. Create this folder:
|
||||
|
||||
```text
|
||||
clipboard-fallback
|
||||
```
|
||||
|
||||
3. Copy these files into it:
|
||||
|
||||
```text
|
||||
manifest.json
|
||||
renderer.js
|
||||
```
|
||||
|
||||
4. Restart Legcord.
|
||||
5. Enable **Clipboard Fallback** in Legcord's plugin settings.
|
||||
|
||||
## Older Legcord workaround: custom bundle
|
||||
|
||||
If your Legcord version does not have filesystem plugins yet, copy `custom-bundle.js` into:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/legcord/custom.js
|
||||
```
|
||||
|
||||
Do **not** use `renderer.js` as `custom.js`; `renderer.js` is the filesystem-plugin entry and expects Legcord's plugin loader to provide `module.exports`.
|
||||
|
||||
and add `"custom"` to the `mods` array in:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/legcord/storage/settings.json
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
"mods": ["equicord", "custom"]
|
||||
```
|
||||
|
||||
Then restart Legcord.
|
||||
216
docs/examples/clipboard-fallback-plugin/custom-bundle.js
Normal file
216
docs/examples/clipboard-fallback-plugin/custom-bundle.js
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
(() => {
|
||||
const module = { exports: {} };
|
||||
const api = {
|
||||
logger: {
|
||||
log: (...args) => console.log("[ClipboardFallback]", ...args),
|
||||
warn: (...args) => console.warn("[ClipboardFallback]", ...args),
|
||||
error: (...args) => console.error("[ClipboardFallback]", ...args),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Legcord Clipboard Fallback
|
||||
*
|
||||
* Discord's web UI uses navigator.clipboard.writeText for actions such as
|
||||
* "Copy User ID" and "Copy Message Link", and navigator.clipboard.write for
|
||||
* richer clipboard payloads such as images. In some Legcord/Electron/macOS
|
||||
* combinations Chromium rejects those calls because the document is not focused
|
||||
* or the clipboard permission is not granted, leaving the clipboard unchanged.
|
||||
*
|
||||
* This renderer plugin replaces those APIs with selection-based copy fallbacks
|
||||
* that run inside the original click gesture.
|
||||
*/
|
||||
module.exports.activate = (api) => {
|
||||
const PATCH_KEY = Symbol.for("legcord.clipboardFallback.installed");
|
||||
|
||||
function install() {
|
||||
try {
|
||||
if (!navigator.clipboard) {
|
||||
api.logger.warn("navigator.clipboard is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.clipboard[PATCH_KEY]) return;
|
||||
|
||||
const originalWriteText = navigator.clipboard.writeText?.bind(navigator.clipboard);
|
||||
const originalWrite = navigator.clipboard.write?.bind(navigator.clipboard);
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
if (typeof FileReader !== "undefined") {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result));
|
||||
reader.onerror = () => reject(reader.error ?? new Error("Failed to read clipboard image"));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
// Test/runtime fallback for environments with Blob but no FileReader.
|
||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
let binary = "";
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
|
||||
return `data:${blob.type || "application/octet-stream"};base64,${base64}`;
|
||||
}
|
||||
|
||||
function selectElementForCopy(element) {
|
||||
const previousActiveElement = document.activeElement;
|
||||
const selection = window.getSelection?.() ?? globalThis.getSelection?.();
|
||||
const range = document.createRange();
|
||||
|
||||
element.focus?.();
|
||||
range.selectNodeContents(element);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const copied = document.execCommand("copy");
|
||||
|
||||
selection?.removeAllRanges();
|
||||
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
|
||||
try {
|
||||
previousActiveElement.focus();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!copied) throw new Error("document.execCommand('copy') returned false");
|
||||
}
|
||||
|
||||
async function fallbackCopyText(text) {
|
||||
const value = String(text);
|
||||
const parent = document.body || document.documentElement;
|
||||
if (!parent) throw new Error("No document body available for clipboard fallback");
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = value;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.setAttribute("aria-hidden", "true");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-9999px";
|
||||
textarea.style.top = "0";
|
||||
textarea.style.width = "1px";
|
||||
textarea.style.height = "1px";
|
||||
textarea.style.opacity = "0";
|
||||
textarea.style.pointerEvents = "none";
|
||||
|
||||
parent.appendChild(textarea);
|
||||
|
||||
const previousActiveElement = document.activeElement;
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, value.length);
|
||||
|
||||
const copied = document.execCommand("copy");
|
||||
textarea.remove();
|
||||
|
||||
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
|
||||
try {
|
||||
previousActiveElement.focus();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!copied) throw new Error("document.execCommand('copy') returned false");
|
||||
}
|
||||
|
||||
async function getClipboardItemType(item, type) {
|
||||
if (!item?.types?.includes(type) || typeof item.getType !== "function") return null;
|
||||
return await item.getType(type);
|
||||
}
|
||||
|
||||
async function fallbackCopyItems(items) {
|
||||
const parent = document.body || document.documentElement;
|
||||
if (!parent) throw new Error("No document body available for clipboard fallback");
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.contentEditable = "true";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
container.style.position = "fixed";
|
||||
container.style.left = "-9999px";
|
||||
container.style.top = "0";
|
||||
container.style.width = "1px";
|
||||
container.style.height = "1px";
|
||||
container.style.overflow = "hidden";
|
||||
|
||||
for (const item of items) {
|
||||
const htmlBlob = await getClipboardItemType(item, "text/html");
|
||||
if (htmlBlob) {
|
||||
container.innerHTML += await htmlBlob.text();
|
||||
continue;
|
||||
}
|
||||
|
||||
const textBlob = await getClipboardItemType(item, "text/plain");
|
||||
if (textBlob) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = await textBlob.text();
|
||||
container.appendChild(span);
|
||||
continue;
|
||||
}
|
||||
|
||||
const imageType = item?.types?.find((type) => type.startsWith("image/"));
|
||||
if (imageType && typeof item.getType === "function") {
|
||||
const imageBlob = await item.getType(imageType);
|
||||
const image = document.createElement("img");
|
||||
image.src = await blobToDataUrl(imageBlob);
|
||||
image.alt = "";
|
||||
container.appendChild(image);
|
||||
}
|
||||
}
|
||||
|
||||
if (!container.innerHTML && !container.textContent && !container.children?.length) {
|
||||
throw new Error("No supported clipboard item types found");
|
||||
}
|
||||
|
||||
parent.appendChild(container);
|
||||
selectElementForCopy(container);
|
||||
container.remove();
|
||||
}
|
||||
|
||||
if (originalWriteText) {
|
||||
Object.defineProperty(navigator.clipboard, "writeText", {
|
||||
configurable: true,
|
||||
value: async (text) => {
|
||||
try {
|
||||
await fallbackCopyText(text);
|
||||
api.logger.log("copied text via fallback", text);
|
||||
} catch (fallbackError) {
|
||||
api.logger.warn("text fallback failed; trying original writeText", fallbackError);
|
||||
return originalWriteText(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (originalWrite) {
|
||||
Object.defineProperty(navigator.clipboard, "write", {
|
||||
configurable: true,
|
||||
value: async (items) => {
|
||||
try {
|
||||
await fallbackCopyItems(items);
|
||||
api.logger.log("copied rich clipboard payload via fallback");
|
||||
} catch (fallbackError) {
|
||||
api.logger.warn("rich clipboard fallback failed; trying original write", fallbackError);
|
||||
return originalWrite(items);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(navigator.clipboard, PATCH_KEY, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: true,
|
||||
});
|
||||
|
||||
api.logger.log("installed");
|
||||
} catch (error) {
|
||||
api.logger.error("install failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
install();
|
||||
window.addEventListener("DOMContentLoaded", install, { once: true });
|
||||
};
|
||||
|
||||
if (typeof module.exports.activate === "function") {
|
||||
module.exports.activate(api);
|
||||
}
|
||||
})();
|
||||
9
docs/examples/clipboard-fallback-plugin/manifest.json
Normal file
9
docs/examples/clipboard-fallback-plugin/manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "clipboard-fallback",
|
||||
"name": "Clipboard Fallback",
|
||||
"version": "1.1.0",
|
||||
"description": "Fixes Discord in-page text copy actions in Legcord by falling back to document.execCommand('copy') when navigator.clipboard.writeText is blocked.",
|
||||
"author": "Nigel Thornberry",
|
||||
"compatibleVersions": ["*"],
|
||||
"renderer": "renderer.js"
|
||||
}
|
||||
201
docs/examples/clipboard-fallback-plugin/renderer.js
Normal file
201
docs/examples/clipboard-fallback-plugin/renderer.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* Legcord Clipboard Fallback
|
||||
*
|
||||
* Discord's web UI uses navigator.clipboard.writeText for actions such as
|
||||
* "Copy User ID" and "Copy Message Link", and navigator.clipboard.write for
|
||||
* richer clipboard payloads such as images. In some Legcord/Electron/macOS
|
||||
* combinations Chromium rejects those calls because the document is not focused
|
||||
* or the clipboard permission is not granted, leaving the clipboard unchanged.
|
||||
*
|
||||
* This renderer plugin replaces those APIs with selection-based copy fallbacks
|
||||
* that run inside the original click gesture.
|
||||
*/
|
||||
module.exports.activate = (api) => {
|
||||
const PATCH_KEY = Symbol.for("legcord.clipboardFallback.installed");
|
||||
|
||||
function install() {
|
||||
try {
|
||||
if (!navigator.clipboard) {
|
||||
api.logger.warn("navigator.clipboard is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.clipboard[PATCH_KEY]) return;
|
||||
|
||||
const originalWriteText = navigator.clipboard.writeText?.bind(navigator.clipboard);
|
||||
const originalWrite = navigator.clipboard.write?.bind(navigator.clipboard);
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
if (typeof FileReader !== "undefined") {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result));
|
||||
reader.onerror = () => reject(reader.error ?? new Error("Failed to read clipboard image"));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
// Test/runtime fallback for environments with Blob but no FileReader.
|
||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
let binary = "";
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
|
||||
return `data:${blob.type || "application/octet-stream"};base64,${base64}`;
|
||||
}
|
||||
|
||||
function selectElementForCopy(element) {
|
||||
const previousActiveElement = document.activeElement;
|
||||
const selection = window.getSelection?.() ?? globalThis.getSelection?.();
|
||||
const range = document.createRange();
|
||||
|
||||
element.focus?.();
|
||||
range.selectNodeContents(element);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const copied = document.execCommand("copy");
|
||||
|
||||
selection?.removeAllRanges();
|
||||
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
|
||||
try {
|
||||
previousActiveElement.focus();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!copied) throw new Error("document.execCommand('copy') returned false");
|
||||
}
|
||||
|
||||
async function fallbackCopyText(text) {
|
||||
const value = String(text);
|
||||
const parent = document.body || document.documentElement;
|
||||
if (!parent) throw new Error("No document body available for clipboard fallback");
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = value;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.setAttribute("aria-hidden", "true");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-9999px";
|
||||
textarea.style.top = "0";
|
||||
textarea.style.width = "1px";
|
||||
textarea.style.height = "1px";
|
||||
textarea.style.opacity = "0";
|
||||
textarea.style.pointerEvents = "none";
|
||||
|
||||
parent.appendChild(textarea);
|
||||
|
||||
const previousActiveElement = document.activeElement;
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, value.length);
|
||||
|
||||
const copied = document.execCommand("copy");
|
||||
textarea.remove();
|
||||
|
||||
if (previousActiveElement && typeof previousActiveElement.focus === "function") {
|
||||
try {
|
||||
previousActiveElement.focus();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!copied) throw new Error("document.execCommand('copy') returned false");
|
||||
}
|
||||
|
||||
async function getClipboardItemType(item, type) {
|
||||
if (!item?.types?.includes(type) || typeof item.getType !== "function") return null;
|
||||
return await item.getType(type);
|
||||
}
|
||||
|
||||
async function fallbackCopyItems(items) {
|
||||
const parent = document.body || document.documentElement;
|
||||
if (!parent) throw new Error("No document body available for clipboard fallback");
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.contentEditable = "true";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
container.style.position = "fixed";
|
||||
container.style.left = "-9999px";
|
||||
container.style.top = "0";
|
||||
container.style.width = "1px";
|
||||
container.style.height = "1px";
|
||||
container.style.overflow = "hidden";
|
||||
|
||||
for (const item of items) {
|
||||
const htmlBlob = await getClipboardItemType(item, "text/html");
|
||||
if (htmlBlob) {
|
||||
container.innerHTML += await htmlBlob.text();
|
||||
continue;
|
||||
}
|
||||
|
||||
const textBlob = await getClipboardItemType(item, "text/plain");
|
||||
if (textBlob) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = await textBlob.text();
|
||||
container.appendChild(span);
|
||||
continue;
|
||||
}
|
||||
|
||||
const imageType = item?.types?.find((type) => type.startsWith("image/"));
|
||||
if (imageType && typeof item.getType === "function") {
|
||||
const imageBlob = await item.getType(imageType);
|
||||
const image = document.createElement("img");
|
||||
image.src = await blobToDataUrl(imageBlob);
|
||||
image.alt = "";
|
||||
container.appendChild(image);
|
||||
}
|
||||
}
|
||||
|
||||
if (!container.innerHTML && !container.textContent && !container.children?.length) {
|
||||
throw new Error("No supported clipboard item types found");
|
||||
}
|
||||
|
||||
parent.appendChild(container);
|
||||
selectElementForCopy(container);
|
||||
container.remove();
|
||||
}
|
||||
|
||||
if (originalWriteText) {
|
||||
Object.defineProperty(navigator.clipboard, "writeText", {
|
||||
configurable: true,
|
||||
value: async (text) => {
|
||||
try {
|
||||
await fallbackCopyText(text);
|
||||
api.logger.log("copied text via fallback", text);
|
||||
} catch (fallbackError) {
|
||||
api.logger.warn("text fallback failed; trying original writeText", fallbackError);
|
||||
return originalWriteText(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (originalWrite) {
|
||||
Object.defineProperty(navigator.clipboard, "write", {
|
||||
configurable: true,
|
||||
value: async (items) => {
|
||||
try {
|
||||
await fallbackCopyItems(items);
|
||||
api.logger.log("copied rich clipboard payload via fallback");
|
||||
} catch (fallbackError) {
|
||||
api.logger.warn("rich clipboard fallback failed; trying original write", fallbackError);
|
||||
return originalWrite(items);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(navigator.clipboard, PATCH_KEY, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: true,
|
||||
});
|
||||
|
||||
api.logger.log("installed");
|
||||
} catch (error) {
|
||||
api.logger.error("install failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
install();
|
||||
window.addEventListener("DOMContentLoaded", install, { once: true });
|
||||
};
|
||||
20
docs/examples/hello-plugin/main.js
Normal file
20
docs/examples/hello-plugin/main.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Main process entry example.
|
||||
* Receives api from Legcord plugin manager.
|
||||
*/
|
||||
module.exports.activate = (api) => {
|
||||
api.logger.log("main entry active");
|
||||
|
||||
// Example: patch BrowserWindow.getTitle globally in main process.
|
||||
const unpatch = api.patcher.after("getTitle", api.electron.BrowserWindow.prototype, (_args, ret) => {
|
||||
if (typeof ret === "string" && !ret.endsWith(" [HelloPlugin]")) {
|
||||
return `${ret} [HelloPlugin]`;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
api.onCleanup(() => {
|
||||
unpatch();
|
||||
api.logger.log("main entry cleaned up");
|
||||
});
|
||||
};
|
||||
11
docs/examples/hello-plugin/manifest.json
Normal file
11
docs/examples/hello-plugin/manifest.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "hello-plugin",
|
||||
"name": "Hello Plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Example plugin showing main/preload/renderer entries and function patching.",
|
||||
"author": "Legcord Docs",
|
||||
"compatibleVersions": ["*"],
|
||||
"main": "main.js",
|
||||
"preload": "preload.js",
|
||||
"renderer": "renderer.js"
|
||||
}
|
||||
29
docs/examples/hello-plugin/preload.js
Normal file
29
docs/examples/hello-plugin/preload.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Preload entry example.
|
||||
* Runs in Legcord preload context with access to DOM and plugin API.
|
||||
*/
|
||||
module.exports.activate = (api) => {
|
||||
api.logger.log("preload entry active");
|
||||
|
||||
// Example: patch document title setter for demonstration.
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, "title");
|
||||
if (!descriptor?.set) return;
|
||||
|
||||
const patchTarget = { setTitle: descriptor.set };
|
||||
const unpatch = api.patcher.before("setTitle", patchTarget, (args) => {
|
||||
if (typeof args[0] === "string" && !args[0].includes("[P]")) {
|
||||
args[0] = `[P] ${args[0]}`;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(document, "title", {
|
||||
configurable: true,
|
||||
get: descriptor.get?.bind(document),
|
||||
set: (value) => patchTarget.setTitle.call(document, value),
|
||||
});
|
||||
|
||||
api.onCleanup(() => {
|
||||
unpatch();
|
||||
api.logger.log("preload entry cleaned up");
|
||||
});
|
||||
};
|
||||
18
docs/examples/hello-plugin/renderer.js
Normal file
18
docs/examples/hello-plugin/renderer.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Renderer entry example.
|
||||
* Runs in the Discord page context.
|
||||
*
|
||||
* In renderer entries, expose activate function through module.exports.
|
||||
*/
|
||||
module.exports.activate = (api) => {
|
||||
api.logger.log("renderer entry active");
|
||||
|
||||
// Example: patch console.log in renderer just to show before/instead/after usage.
|
||||
const unpatchBefore = api.patcher.before("log", console, (args) => {
|
||||
args.unshift("[HelloPlugin]");
|
||||
});
|
||||
|
||||
const unpatchAfter = api.patcher.after("log", console, (_args, ret) => ret);
|
||||
void unpatchBefore;
|
||||
void unpatchAfter;
|
||||
};
|
||||
130
docs/plugin-system.md
Normal file
130
docs/plugin-system.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Legcord Plugin System
|
||||
|
||||
Legcord now supports filesystem plugins loaded from:
|
||||
|
||||
`<userData>/plugins/<plugin-id>/`
|
||||
|
||||
Each plugin can provide separate runtime entries for:
|
||||
|
||||
- `main` (Electron main process)
|
||||
- `preload` (Legcord preload context)
|
||||
- `renderer` (Discord page context)
|
||||
|
||||
## Manifest
|
||||
|
||||
Create a `manifest.json` in your plugin folder:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "example-plugin",
|
||||
"name": "Example Plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Example plugin with all three targets",
|
||||
"author": "you",
|
||||
"compatibleVersions": ["1.3.x"],
|
||||
"main": "main.js",
|
||||
"preload": "preload.js",
|
||||
"renderer": "renderer.js"
|
||||
}
|
||||
```
|
||||
|
||||
Required fields:
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `version`
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `description`
|
||||
- `author`
|
||||
- `compatibleVersions`
|
||||
- `main`
|
||||
- `preload`
|
||||
- `renderer`
|
||||
|
||||
`compatibleVersions` supports exact versions and `x` wildcard prefixes:
|
||||
|
||||
- `"1.3.0"` (exact)
|
||||
- `"1.3.x"` (any patch in `1.3`)
|
||||
- `"*"` (all versions)
|
||||
|
||||
If the running Legcord version does not match, the plugin is marked incompatible and cannot be enabled/loaded.
|
||||
Plugins are disabled by default until explicitly enabled in the Plugins settings page.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Main and preload entries can export either:
|
||||
|
||||
- `activate(api)` named export
|
||||
- default export function
|
||||
|
||||
Renderer entries run as plain script files and can expose:
|
||||
|
||||
- `module.exports.activate = (api) => { ... }`
|
||||
- `module.exports.default = (api) => { ... }`
|
||||
- `globalThis.activatePlugin = (api) => { ... }`
|
||||
|
||||
## Plugin Control API
|
||||
|
||||
From `window.legcord.plugins`:
|
||||
|
||||
- `list()`
|
||||
- `setEnabled(id, enabled)`
|
||||
- `reload(id)`
|
||||
|
||||
Main-process entries are enabled/disabled live. Preload/renderer entries are loaded on startup/navigation and will reflect enable state on next load.
|
||||
|
||||
## Patcher API (Spitroast-style)
|
||||
|
||||
Plugin APIs include:
|
||||
|
||||
- `api.patcher.before(name, parent, callback, oneTime?)`
|
||||
- `api.patcher.after(name, parent, callback, oneTime?)`
|
||||
- `api.patcher.instead(name, parent, callback, oneTime?)`
|
||||
|
||||
Callbacks use the same semantics as Spitroast (`args` mutation/replacement, return value replacement, instead chaining).
|
||||
The return value is an `unpatch()` function.
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
export function activate(api) {
|
||||
const unpatch = api.patcher.before("fetch", window, (args) => {
|
||||
api.logger.log("fetch called with", args[0]);
|
||||
});
|
||||
|
||||
api.onCleanup(() => {
|
||||
unpatch();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Plugin enable state is stored in config under `pluginStates`.
|
||||
- Plugin folders are scanned from disk at startup.
|
||||
- Invalid manifests or missing entry files are skipped safely.
|
||||
|
||||
## Full Example Plugin
|
||||
|
||||
A complete example is available at:
|
||||
|
||||
- `docs/examples/hello-plugin/manifest.json`
|
||||
- `docs/examples/hello-plugin/main.js`
|
||||
- `docs/examples/hello-plugin/preload.js`
|
||||
- `docs/examples/hello-plugin/renderer.js`
|
||||
|
||||
A practical renderer-only workaround plugin is also available at:
|
||||
|
||||
- `docs/examples/clipboard-fallback-plugin/manifest.json`
|
||||
- `docs/examples/clipboard-fallback-plugin/renderer.js`
|
||||
|
||||
To test it:
|
||||
|
||||
1. Copy `docs/examples/hello-plugin` into your runtime plugins directory:
|
||||
- `<userData>/plugins/hello-plugin`
|
||||
2. Restart Legcord or use the Plugins settings page:
|
||||
- enable/disable
|
||||
- reload
|
||||
3. Open DevTools and watch for `[Plugin:hello-plugin]` logs.
|
||||
|
|
@ -1,10 +1,33 @@
|
|||
import type { Configuration } from "electron-builder";
|
||||
|
||||
import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
|
||||
import debianLicence from "./scripts/spdxLicenceDebianFormat";
|
||||
import { ACTION_FRIENDLY_NAMES, EXCLUDED_FROM_SHORTCUTS, ValidActions } from "./src/common/commandDefinitions";
|
||||
|
||||
const desktopActions = (exec: "AppRun" | "/opt/Legcord/legcord") =>
|
||||
Object.fromEntries(
|
||||
(Object.values(ValidActions) as ValidActions[])
|
||||
.filter((action) => !EXCLUDED_FROM_SHORTCUTS.includes(action))
|
||||
.map((action) => [
|
||||
action,
|
||||
{
|
||||
Name: ACTION_FRIENDLY_NAMES[action],
|
||||
Exec: `${exec} --${action} %U`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const availableActions = (Object.values(ValidActions) as ValidActions[])
|
||||
.filter((action) => !EXCLUDED_FROM_SHORTCUTS.includes(action))
|
||||
.join(";");
|
||||
|
||||
export const config: Configuration = {
|
||||
appId: "app.legcord.Legcord",
|
||||
productName: "Legcord",
|
||||
// Biome treats electron-builder macro placeholders as template syntax.
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: electron-builder expands these placeholders.
|
||||
artifactName: "Legcord-${version}-${os}-${arch}.${ext}",
|
||||
beforePack: "./scripts/build/sandboxFix.cjs",
|
||||
beforePack: applyAppImageSandboxFix,
|
||||
protocols: [
|
||||
{
|
||||
name: "Discord",
|
||||
|
|
@ -18,6 +41,8 @@ export const config: Configuration = {
|
|||
extendInfo: {
|
||||
NSMicrophoneUsageDescription: "Legcord requires access to the microphone to function properly.",
|
||||
NSCameraUsageDescription: "Legcord requires access to the camera to function properly.",
|
||||
NSAudioCaptureUsageDescription:
|
||||
"Legcord requires access to system audio to share sound during screenshare.",
|
||||
NSCameraUseContinuityCameraDeviceType: true,
|
||||
"com.apple.security.device.audio-input": true,
|
||||
"com.apple.security.device.camera": true,
|
||||
|
|
@ -37,6 +62,19 @@ export const config: Configuration = {
|
|||
},
|
||||
},
|
||||
|
||||
appImage: {
|
||||
desktop: {
|
||||
entry: {
|
||||
Actions: availableActions,
|
||||
},
|
||||
desktopActions: desktopActions("AppRun"),
|
||||
},
|
||||
},
|
||||
|
||||
pacman: {
|
||||
depends: ["gtk3", "libnotify", "xdg-utils", "at-spi2-core", "alsa-lib", "nspr", "nss"],
|
||||
},
|
||||
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
|
|
@ -64,7 +102,14 @@ export const config: Configuration = {
|
|||
deb: {
|
||||
category: "Network",
|
||||
icon: "build/icon.icns",
|
||||
depends: ["libgbm-dev", "libasound2", "libnspr4", "libnss3"],
|
||||
depends: ["libasound2", "libnspr4", "libnss3", "libasound2t64", "libasound2-plugins"],
|
||||
desktop: {
|
||||
entry: {
|
||||
Actions: availableActions,
|
||||
},
|
||||
desktopActions: desktopActions("/opt/Legcord/legcord"),
|
||||
},
|
||||
fpm: [`${debianLicence()}=/usr/share/doc/legcord/copyright`],
|
||||
},
|
||||
|
||||
files: [
|
||||
|
|
@ -81,6 +126,20 @@ export const config: Configuration = {
|
|||
electronDownload: {
|
||||
cache: ".cache",
|
||||
},
|
||||
|
||||
electronFuses: {
|
||||
runAsNode: false,
|
||||
enableCookieEncryption: false,
|
||||
enableNodeOptionsEnvironmentVariable: false,
|
||||
enableNodeCliInspectArguments: false,
|
||||
enableEmbeddedAsarIntegrityValidation: false,
|
||||
onlyLoadAppFromAsar: true,
|
||||
loadBrowserProcessSpecificV8Snapshot: false,
|
||||
grantFileProtocolExtraPrivileges: false,
|
||||
},
|
||||
toolsets: {
|
||||
appimage: "1.0.3",
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<id>app.legcord.Legcord</id>
|
||||
<name>Legcord</name>
|
||||
<summary>Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.</summary>
|
||||
<developer>
|
||||
<developer id="app.legcord">
|
||||
<name>Legcord Contributors</name>
|
||||
</developer>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
|
|
@ -16,19 +16,63 @@
|
|||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Legcord settings page on MacOS</caption>
|
||||
<image type="source">https://github.com/Legcord/Legcord/blob/77e2ccafb221936a99654c237cb385d486780bc7/assets/screenshots/settings.png</image>
|
||||
<caption>Legcord settings page on macOS</caption>
|
||||
<image type="source">https://raw.githubusercontent.com/Legcord/Legcord/refs/tags/v1.2.4/assets/screenshots/settings.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>Legcord settings open with Shelter configs</caption>
|
||||
<image type="source">https://github.com/Legcord/Legcord/blob/77e2ccafb221936a99654c237cb385d486780bc7/assets/screenshots/shelter.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/Legcord/Legcord/refs/tags/v1.2.4/assets/screenshots/shelter.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>Legcord settings open with custom keybind settings shown</caption>
|
||||
<image type="source">https://github.com/Legcord/Legcord/blob/77e2ccafb221936a99654c237cb385d486780bc7/assets/screenshots/keybinds.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/Legcord/Legcord/refs/tags/v1.2.4/assets/screenshots/keybinds.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="1.2.4" date="2026-04-15" type="stable">
|
||||
<url>https://github.com/Legcord/Legcord/releases/tag/v1.2.4</url>
|
||||
<description>
|
||||
<p>What’s New in Legcord v1.2.4 ✨</p>
|
||||
<ul>
|
||||
<li>🐧 **Huge streaming performance improvements on Linux**</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>🔄 **Separate “No Bundle Updates” setting for each client mod**</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>💾 **Backup & Restore added**</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>🎮 **Improved macOS game detection**</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>🛡️ **New CSP options setting**</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>🪟 **Window material setting added** for supported systems ([https://github.com/Legcord/Legcord/pull/1025](https://github.com/Legcord/Legcord/pull/1025) by @passionvine).</li>
|
||||
<li>📦 **OPTIONAL automatic Linux updates added** ([https://github.com/Legcord/Legcord/pull/1027](https://github.com/Legcord/Legcord/pull/1027) by @KarboXXX).</li>
|
||||
<li>🎛️ **Support banner popup added**</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>🖥️ Fixed Linux `StartupWMClass` to `legcord` ([https://github.com/Legcord/Legcord/pull/1021](https://github.com/Legcord/Legcord/pull/1021) by @khancyr).</li>
|
||||
<li>📺 Fixed screensharing quality, FPS, and audio ([https://github.com/Legcord/Legcord/pull/1026](https://github.com/Legcord/Legcord/pull/1026) by @KarboXXX).</li>
|
||||
<li>🎚️ Fixed scrollbar styling on file preview ([https://github.com/Legcord/Legcord/pull/1024](https://github.com/Legcord/Legcord/pull/1024) by @MahmodZE).</li>
|
||||
<li>🔊 Fixed mute toggle sound effect playback ([https://github.com/Legcord/Legcord/pull/1031](https://github.com/Legcord/Legcord/pull/1031) by @psw01).</li>
|
||||
<li>🎮 Fixed RPC status not disappearing properly ([https://github.com/Legcord/Legcord/pull/1032](https://github.com/Legcord/Legcord/pull/1032) by @Adrigamer2950).</li>
|
||||
<li>🔐 Fixed security prompt appearing with no passkey ([https://github.com/Legcord/Legcord/pull/1033](https://github.com/Legcord/Legcord/pull/1033) by @dequeues).</li>
|
||||
<li>🇫🇷 Added missing French translations for settings categories ([https://github.com/Legcord/Legcord/pull/1028](https://github.com/Legcord/Legcord/pull/1028) by @youtsuhodev).</li>
|
||||
<li>⌨️ Polished keybinds UI ([https://github.com/Legcord/Legcord/pull/1038](https://github.com/Legcord/Legcord/pull/1038) by @youtsuhodev).</li>
|
||||
<li>🎥 Fixed camera/mic device switching not working ([https://github.com/Legcord/Legcord/pull/1051](https://github.com/Legcord/Legcord/pull/1051) by @narasaka).</li>
|
||||
<li>⚙️ Fixed settings shortcut behavior ([https://github.com/Legcord/Legcord/pull/1046](https://github.com/Legcord/Legcord/pull/1046) by @KirobotDev).</li>
|
||||
<li>📏 Fixed dropdown height limit in Games settings ([https://github.com/Legcord/Legcord/pull/1045](https://github.com/Legcord/Legcord/pull/1045) by @youtsuhodev).</li>
|
||||
<li>📐 Fixed dropdown max height calculation ([https://github.com/Legcord/Legcord/pull/1059](https://github.com/Legcord/Legcord/pull/1059) by @MahmodZE).</li>
|
||||
<li>🧹 Cleanup of old fix-me notes and code polish ([https://github.com/Legcord/Legcord/pull/1060](https://github.com/Legcord/Legcord/pull/1060) by @KirobotDev).</li>
|
||||
<li>📂 Fixed restore zip path traversal issue ([https://github.com/Legcord/Legcord/pull/1061](https://github.com/Legcord/Legcord/pull/1061) by @youtsuhodev).</li>
|
||||
<li>🛡️ Added stricter CSP, better permission handling, and mod validation ([https://github.com/Legcord/Legcord/pull/1069](https://github.com/Legcord/Legcord/pull/1069) by @KirobotDev).</li>
|
||||
<li>Fixed first time setup</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.2.3" date="2026-04-15" type="stable">
|
||||
<url>https://github.com/Legcord/Legcord/releases/tag/v1.2.3</url>
|
||||
<description>
|
||||
|
|
@ -231,10 +275,6 @@
|
|||
<url>https://github.com/Legcord/Legcord/releases/tag/v1.0.2</url>
|
||||
<description/>
|
||||
</release>
|
||||
<release version="devbuild" date="2024-10-12" type="development">
|
||||
<url>https://github.com/Legcord/Legcord/releases/tag/devbuild</url>
|
||||
<description/>
|
||||
</release>
|
||||
<release version="v1.0.0" date="2024-10-11" type="stable">
|
||||
<url>https://github.com/Legcord/Legcord/releases/tag/v1.0.0</url>
|
||||
<description/>
|
||||
|
|
|
|||
64
package.json
64
package.json
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "legcord",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"description": "Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.",
|
||||
"main": "ts-out/main.js",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": ">=26"
|
||||
},
|
||||
"scripts": {
|
||||
"build:dev": "rollup -c --environment BUILD:dev && tsx scripts/copyVenmic.mts",
|
||||
"build:dev": "rollup -c --environment BUILD:dev && node --experimental-strip-types scripts/copyVenmic.ts",
|
||||
"build:plugins": "lune ci --repoSubDir src/shelter --to ts-out/plugins",
|
||||
"build": "pnpm build:plugins && rolldown -c rolldown.config.js && tsx scripts/copyVenmic.mts",
|
||||
"build": "pnpm build:plugins && rolldown -c rolldown.config.ts && node --experimental-strip-types scripts/copyVenmic.ts",
|
||||
"start": "pnpm run build && electron --trace-warnings --ozone-platform-hint=auto ./ts-out/main.js",
|
||||
"startThemeManager": "pnpm run build:dev && electron ./ts-out/main.js themes",
|
||||
"package": "pnpm run build && electron-builder",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"lint:fix": "biome check --write",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"CIbuild": "pnpm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip",
|
||||
"updateMeta": "tsx scripts/utils/updateMeta.mts"
|
||||
"updateMeta": "node --experimental-strip-types scripts/utils/updateMeta.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -32,39 +32,45 @@
|
|||
},
|
||||
"homepage": "https://github.com/Legcord/Legcord#readme",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@rolldown-plugin/solid": "^0.0.4",
|
||||
"@rollup/plugin-esm-shim": "^0.1.7",
|
||||
"@biomejs/biome": "2.5.5",
|
||||
"@rolldown-plugin/solid": "^0.1.0",
|
||||
"@rollup/plugin-esm-shim": "^0.1.8",
|
||||
"@types/adm-zip": "^0.5.8",
|
||||
"@types/minimatch": "^6.0.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@uwu/lune": "^1.6.2",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@uwu/lune": "^1.6.3",
|
||||
"@uwu/shelter-defs": "^1.5.1",
|
||||
"babel-preset-solid": "^1.9.3",
|
||||
"electron": "41.2.0",
|
||||
"electron-builder": "26.0.20",
|
||||
"lucide-solid": "^0.475.0",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"@uwu/shelter-ui": "^0.0.6",
|
||||
"@xmldom/xmldom": "^0.9.10",
|
||||
"app-builder-lib": "^26.15.3",
|
||||
"babel-preset-solid": "^1.9.12",
|
||||
"electron": "43.2.0",
|
||||
"electron-builder": "26.15.7",
|
||||
"lucide-solid": "^1.26.0",
|
||||
"monaco-editor": "0.56.0",
|
||||
"rolldown": "1.2.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"solid-js": "^1.9.3",
|
||||
"solid-motionone": "^1.0.3",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"xml-formatter": "^3.6.6"
|
||||
"solid-js": "^1.9.14",
|
||||
"solid-motionone": "^1.0.4",
|
||||
"spdx-license-list": "^6.11.0",
|
||||
"typescript": "^7.0.2",
|
||||
"xml-formatter": "^3.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"arrpc": "https://github.com/Legcord/arrpc.git#efe7589762470d32b9ba10d529be5acc23cd0e19",
|
||||
"electron-context-menu": "^4.0.4",
|
||||
"@jellybrick/dbus-next": "^0.11.1",
|
||||
"adm-zip": "^0.6.0",
|
||||
"arrpc": "github:Legcord/arrpc#efe7589762470d32b9ba10d529be5acc23cd0e19",
|
||||
"electron-context-menu": "^4.1.2",
|
||||
"electron-is-dev": "^3.0.1",
|
||||
"electron-updater": "^6.8.9",
|
||||
"ms": "^2.1.3",
|
||||
"spitroast": "^2.1.6",
|
||||
"tslib": "^2.8.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.21.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vencord/venmic": "^6.1.0"
|
||||
"@vencord/venmic": "^7.1.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@11.10.0",
|
||||
"package-manager-strict": false
|
||||
}
|
||||
|
|
|
|||
4110
pnpm-lock.yaml
generated
4110
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
12
pnpm-workspace.yaml
Normal file
12
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
allowBuilds:
|
||||
"@parcel/watcher": true
|
||||
"@vencord/venmic": true
|
||||
electron-winstaller: true
|
||||
esbuild: true
|
||||
koffi: true
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
- electron@42.3.3 || 43.2.0
|
||||
|
||||
overrides:
|
||||
yauzl: "^3.3.1" # Electron still can't get their shit together https://github.com/electron/electron/issues/51619
|
||||
|
|
@ -31,9 +31,13 @@ export default defineConfig([
|
|||
targets: [
|
||||
{ src: "src/**/**/*.html", dest: "ts-out/html/" },
|
||||
{ src: "src/**/**/*.css", dest: "ts-out/css/" },
|
||||
{ src: "src/setup/setup.css", dest: "ts-out/html/" },
|
||||
{ src: "node_modules/@uwu/shelter-ui/compat.css", dest: "ts-out/html/" },
|
||||
{ src: "src/**/**/*.js", dest: "ts-out/js/" },
|
||||
{ src: "package.json", dest: "ts-out/" },
|
||||
{ src: "assets/**/**", dest: "ts-out/assets/" },
|
||||
// Monaco AMD tree for the offline Quick CSS editor (next to editor.html)
|
||||
{ src: "node_modules/monaco-editor/min/vs/**/*", dest: "ts-out/html/monaco/vs" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
|
@ -6,18 +6,21 @@
|
|||
|
||||
// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
|
||||
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import AppImageTarget from "app-builder-lib/out/targets/appimage/AppImageTarget.js";
|
||||
|
||||
let isApplied = false;
|
||||
|
||||
const hook = async () => {
|
||||
if (isApplied) return;
|
||||
isApplied = true;
|
||||
export async function applyAppImageSandboxFix() {
|
||||
if (process.platform !== "linux") {
|
||||
// this fix is only required on linux
|
||||
return;
|
||||
}
|
||||
const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget.js");
|
||||
|
||||
if (isApplied) return;
|
||||
isApplied = true;
|
||||
|
||||
const oldBuildMethod = AppImageTarget.default.prototype.build;
|
||||
AppImageTarget.default.prototype.build = async function (...args) {
|
||||
console.log("Running AppImage builder hook", args);
|
||||
|
|
@ -69,6 +72,4 @@ exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ]
|
|||
|
||||
return ret;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = hook;
|
||||
}
|
||||
66
scripts/spdxLicenceDebianFormat.ts
Normal file
66
scripts/spdxLicenceDebianFormat.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// https://www.debian.org/doc/debian-policy/ch-docs.html#copyright-information
|
||||
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import spdxLicenseList from "spdx-license-list";
|
||||
|
||||
interface CopyrightConfig {
|
||||
upstreamName: string;
|
||||
source: string;
|
||||
copyrightHolder: string;
|
||||
licenseId: string;
|
||||
comment: string; // optional but nice to have
|
||||
}
|
||||
|
||||
function indentText(text: string): string {
|
||||
// formatted text fields follow the same rules as Debian control file
|
||||
// long descriptions: each line indented with a single space, blank
|
||||
// lines represented as " ."
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => (line.trim() === "" ? " ." : ` ${line}`))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function generateCopyrightFile(config: CopyrightConfig): string {
|
||||
const license = spdxLicenseList[config.licenseId];
|
||||
if (!license) {
|
||||
throw new Error(`Unknown SPDX license id: ${config.licenseId}`);
|
||||
}
|
||||
|
||||
const header = [
|
||||
"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/",
|
||||
`Upstream-Name: ${config.upstreamName}`,
|
||||
`Source: ${config.source}`,
|
||||
`Comment: ${indentText(config.comment)}`, // optional field
|
||||
].join("\n");
|
||||
|
||||
const licenseText = readFileSync("license.txt").toString();
|
||||
|
||||
const licenseStanza = [`License: ${config.licenseId}`, indentText(licenseText)].join("\n");
|
||||
|
||||
const filesStanza = ["Files: *", `Copyright: ${config.copyrightHolder}`, `License: ${config.licenseId}`].join("\n");
|
||||
|
||||
return `${[header, filesStanza, licenseStanza].join("\n\n")}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Debian SPDX-formatted license for given license ID
|
||||
*
|
||||
* @export
|
||||
* @return {*} {string}
|
||||
*/
|
||||
export default function (): string {
|
||||
const copyrightContent: string = generateCopyrightFile({
|
||||
upstreamName: "Legcord",
|
||||
source: "https://github.com/Legcord/Legcord",
|
||||
copyrightHolder: `2020 - ${new Date().getFullYear()} Legcord Contributors`, // git log --reverse
|
||||
licenseId: "OSL-3.0",
|
||||
comment: "Open-Source Discord client alternative.",
|
||||
});
|
||||
|
||||
const licensePath = path.join(__dirname, "spdx-license.txt");
|
||||
writeFileSync(licensePath, copyrightContent);
|
||||
return licensePath;
|
||||
}
|
||||
|
|
@ -50,6 +50,17 @@ const latestReleaseInformation = await fetch("https://api.github.com/repos/Legco
|
|||
},
|
||||
}).then((res) => res.json());
|
||||
|
||||
// Ignore devbuild releases
|
||||
if (
|
||||
latestReleaseInformation.name?.toLowerCase().includes("devbuild") ||
|
||||
latestReleaseInformation.tag_name?.toLowerCase().includes("devbuild") ||
|
||||
latestReleaseInformation.prerelease ||
|
||||
latestReleaseInformation.draft
|
||||
) {
|
||||
console.log("Latest release is a devbuild, nothing to be done");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const metaInfo = await fs.readFile("./meta/app.legcord.Legcord.metainfo.xml", "utf-8");
|
||||
|
||||
const parser = new DOMParser().parseFromString(metaInfo, "text/xml");
|
||||
1
src/@types/keybind.d.ts
vendored
1
src/@types/keybind.d.ts
vendored
|
|
@ -6,6 +6,7 @@ export type KeybindActions =
|
|||
| "navigateForward"
|
||||
| "openQuickCss"
|
||||
| "pushToTalk"
|
||||
| "openSettings"
|
||||
| "runJavascript";
|
||||
export interface Keybind {
|
||||
accelerator: Electron.Accelerator;
|
||||
|
|
|
|||
24
src/@types/legcordWindow.d.ts
vendored
24
src/@types/legcordWindow.d.ts
vendored
|
|
@ -5,6 +5,21 @@ import type { Keybind } from "./keybind.js";
|
|||
import type { Settings } from "./settings.js";
|
||||
import type { ThemeManifest } from "./themeManifest.js";
|
||||
|
||||
export interface LegcordPluginInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
enabled: boolean;
|
||||
compatible: boolean;
|
||||
compatibilityMessage?: string;
|
||||
compatibleVersions: string[];
|
||||
hasMain: boolean;
|
||||
hasPreload: boolean;
|
||||
hasRenderer: boolean;
|
||||
}
|
||||
|
||||
export interface LegcordWindow {
|
||||
window: {
|
||||
show: () => void;
|
||||
|
|
@ -31,6 +46,8 @@ export interface LegcordWindow {
|
|||
openCustomIconDialog: () => void;
|
||||
copyDebugInfo: () => void;
|
||||
copyGPUInfo: () => void;
|
||||
openWebRTCInternals: () => void;
|
||||
openGPUInfo: () => void;
|
||||
setLang(lang: string): () => void;
|
||||
addKeybind: (keybind: Keybind) => void;
|
||||
toggleKeybind: (id: string) => void;
|
||||
|
|
@ -63,6 +80,7 @@ export interface LegcordWindow {
|
|||
uninstall: (id: string) => void;
|
||||
set: (id: string, state: boolean) => void;
|
||||
getThemes: () => Readonly<ThemeManifest[]>;
|
||||
refresh: () => Readonly<ThemeManifest[]>;
|
||||
openQuickCss: () => void;
|
||||
edit: (id: string) => void;
|
||||
folder: (id: string) => void;
|
||||
|
|
@ -91,6 +109,12 @@ export interface LegcordWindow {
|
|||
save(data: string): Promise<{ ok: true } | { ok: false; error: string }>;
|
||||
restore(): Promise<string>;
|
||||
};
|
||||
plugins: {
|
||||
list: () => Promise<LegcordPluginInfo[]>;
|
||||
setEnabled: (id: string, enabled: boolean) => Promise<{ ok: boolean }>;
|
||||
reload: (id: string) => Promise<{ ok: boolean }>;
|
||||
openFolder: () => void;
|
||||
};
|
||||
/** Plugin storage API. Requires user to enable "Extended plugin abilities" in Legcord settings. */
|
||||
fs: {
|
||||
writeFile: (
|
||||
|
|
|
|||
30
src/@types/settings.d.ts
vendored
30
src/@types/settings.d.ts
vendored
|
|
@ -25,6 +25,9 @@ export interface AudioSettings {
|
|||
loopbackType: "loopback" | "loopbackWithMute";
|
||||
}
|
||||
|
||||
/** Chromium/Electron proxy modes — mirrors browser proxy settings. */
|
||||
export type ProxyMode = "system" | "direct" | "fixed_servers" | "pac_script" | "auto_detect";
|
||||
|
||||
export interface Settings {
|
||||
// Referenced for detecting a broken config.
|
||||
"0"?: string;
|
||||
|
|
@ -43,12 +46,24 @@ export interface Settings {
|
|||
mods: ValidMods[];
|
||||
mobileMode: boolean;
|
||||
skipSplash: boolean;
|
||||
performanceMode: "battery" | "dynamic" | "performance" | "smoothScreenshare" | "none";
|
||||
performanceMode:
|
||||
| "battery"
|
||||
| "dynamic"
|
||||
| "performance"
|
||||
| "balanced"
|
||||
| "memory"
|
||||
| "voip"
|
||||
| "latency"
|
||||
| "smoothScreenshare"
|
||||
| "none";
|
||||
customJsBundle: RequestInfo | URL | string;
|
||||
customCssBundle: RequestInfo | URL | string;
|
||||
startMinimized: boolean;
|
||||
/** How the main window appears on launch: normal, taskbar minimized, or tray-only. */
|
||||
startMinimized: "off" | "minimized" | "tray";
|
||||
keybinds: Keybind[];
|
||||
hardwareAcceleration: boolean;
|
||||
/** Rewrite H.264 Constrained Baseline (42e0) → Baseline (4200) in WebRTC SDP for HW encode. */
|
||||
sdpH264BaselineRewrite: boolean;
|
||||
useMacSystemPicker: boolean;
|
||||
inviteWebsocket: boolean;
|
||||
disableAutogain: boolean;
|
||||
|
|
@ -67,6 +82,14 @@ export interface Settings {
|
|||
quickCss: boolean;
|
||||
autoScroll: boolean;
|
||||
additionalArguments: string;
|
||||
/** How Legcord resolves HTTP(S) proxies (Chromium + main-process fetch). */
|
||||
proxyMode: ProxyMode;
|
||||
/** Fixed proxy rules, e.g. `http://127.0.0.1:8080` or `socks5://host:1080`. */
|
||||
proxyRules: string;
|
||||
/** Hosts that bypass the proxy (comma-separated), e.g. `<local>,*.intranet.example`. */
|
||||
proxyBypassRules: string;
|
||||
/** PAC script URL when proxyMode is `pac_script`. */
|
||||
proxyPacScript: string;
|
||||
noBundleUpdates: ValidMods[];
|
||||
automaticUpdates: boolean;
|
||||
overlayButtonColor: string;
|
||||
|
|
@ -76,4 +99,7 @@ export interface Settings {
|
|||
modCache?: Record<ValidMods, string>;
|
||||
extendedPluginAbilities: boolean;
|
||||
supportBannerDismissed: boolean;
|
||||
pluginStates?: Record<string, boolean>;
|
||||
// Remove below once the plugin system is fully implemented.
|
||||
showExperimentalPluginMenu: boolean;
|
||||
}
|
||||
|
|
|
|||
2
src/@types/windowState.d.ts
vendored
2
src/@types/windowState.d.ts
vendored
|
|
@ -4,4 +4,6 @@ export interface WindowState {
|
|||
x: number;
|
||||
y: number;
|
||||
isMaximized: boolean;
|
||||
displayId?: number;
|
||||
displayScaleFactor?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { app } from "electron";
|
||||
|
|
@ -38,6 +38,7 @@ export interface BackupManifest extends BackupSavePayload {
|
|||
export interface BackupPaths {
|
||||
userDataPath: string;
|
||||
themesPath: string;
|
||||
extensionsPath: string;
|
||||
pluginsPath: string;
|
||||
pluginStoragePath: string;
|
||||
quickCssPath: string;
|
||||
|
|
@ -114,7 +115,8 @@ export function buildBackupZipBuffer(payload: BackupSavePayload, paths: BackupPa
|
|||
entries.push(...walkFiles(paths.themesPath, "data/themes"));
|
||||
}
|
||||
if (includes.legcordExtensionPlugins) {
|
||||
entries.push(...walkFiles(paths.pluginsPath, "data/plugins"));
|
||||
entries.push(...walkFiles(paths.extensionsPath, "data/plugins"));
|
||||
entries.push(...walkFiles(paths.pluginsPath, "data/runtime-plugins"));
|
||||
entries.push(...walkFiles(paths.pluginStoragePath, "data/plugin-storage"));
|
||||
}
|
||||
if (includes.modBundles) {
|
||||
|
|
@ -197,7 +199,7 @@ export function applyBackupFromMap(
|
|||
if (name.startsWith("data/plugins/")) {
|
||||
if (!inc.legcordExtensionPlugins) continue;
|
||||
const rest = name.slice("data/plugins/".length);
|
||||
const dest = resolvePathUnderBaseDir(paths.pluginsPath, rest);
|
||||
const dest = resolvePathUnderBaseDir(paths.extensionsPath, rest);
|
||||
if (!dest) {
|
||||
console.warn(`[backup] Skipping unsafe zip path (plugins): ${name}`);
|
||||
continue;
|
||||
|
|
@ -206,6 +208,18 @@ export function applyBackupFromMap(
|
|||
continue;
|
||||
}
|
||||
|
||||
if (name.startsWith("data/runtime-plugins/")) {
|
||||
if (!inc.legcordExtensionPlugins) continue;
|
||||
const rest = name.slice("data/runtime-plugins/".length);
|
||||
const dest = resolvePathUnderBaseDir(paths.pluginsPath, rest);
|
||||
if (!dest) {
|
||||
console.warn(`[backup] Skipping unsafe zip path (runtime-plugins): ${name}`);
|
||||
continue;
|
||||
}
|
||||
writeFileEnsuringDirs(dest, data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.startsWith("data/plugin-storage/")) {
|
||||
if (!inc.legcordExtensionPlugins) continue;
|
||||
const rest = name.slice("data/plugin-storage/".length);
|
||||
|
|
|
|||
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];
|
||||
|
|
@ -10,9 +10,9 @@ export let firstRun: boolean;
|
|||
// Performance optimization: Cache config to avoid reading file on every call
|
||||
let configCache: Settings | null = null;
|
||||
let configCacheTime = 0;
|
||||
const CONFIG_CACHE_TTL = 1000; // Cache for 1 second
|
||||
const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds
|
||||
const defaults: Settings = {
|
||||
windowStyle: "default",
|
||||
windowStyle: "overlay",
|
||||
channel: "stable",
|
||||
bounceOnPing: false,
|
||||
csp: "none",
|
||||
|
|
@ -34,22 +34,23 @@ const defaults: Settings = {
|
|||
loopbackType: "loopback",
|
||||
},
|
||||
multiInstance: false,
|
||||
mods: ["vencord"],
|
||||
mods: [],
|
||||
transparency: "none",
|
||||
windowMaterial: "mica",
|
||||
spellcheck: true,
|
||||
hardwareAcceleration: true,
|
||||
sdpH264BaselineRewrite: true,
|
||||
performanceMode: "none",
|
||||
skipSplash: false,
|
||||
inviteWebsocket: true,
|
||||
startMinimized: false,
|
||||
startMinimized: "off",
|
||||
disableHttpCache: false,
|
||||
customJsBundle: "https://legcord.app/placeholder.js",
|
||||
customCssBundle: "https://legcord.app/placeholder.css",
|
||||
disableAutogain: false,
|
||||
autoHideMenuBar: true,
|
||||
blockPowerSavingInVoiceChat: false,
|
||||
useMacSystemPicker: true,
|
||||
useMacSystemPicker: false,
|
||||
mobileMode: false,
|
||||
tray: "dynamic",
|
||||
doneSetup: false,
|
||||
|
|
@ -60,6 +61,10 @@ const defaults: Settings = {
|
|||
noBundleUpdates: [],
|
||||
automaticUpdates: false,
|
||||
additionalArguments: "",
|
||||
proxyMode: "system",
|
||||
proxyRules: "",
|
||||
proxyBypassRules: "<local>",
|
||||
proxyPacScript: "",
|
||||
customIcon: join(import.meta.dirname, "../", "/assets/desktop.png"),
|
||||
smoothScroll: true,
|
||||
autoScroll: false,
|
||||
|
|
@ -67,6 +72,8 @@ const defaults: Settings = {
|
|||
extendedPluginAbilities: false,
|
||||
quickCss: true,
|
||||
supportBannerDismissed: false,
|
||||
showExperimentalPluginMenu: false,
|
||||
pluginStates: {},
|
||||
};
|
||||
|
||||
const safeMode: Settings = {
|
||||
|
|
@ -75,10 +82,12 @@ const safeMode: Settings = {
|
|||
windowStyle: "native",
|
||||
csp: "vanilla",
|
||||
hardwareAcceleration: false,
|
||||
sdpH264BaselineRewrite: false,
|
||||
disableHttpCache: true,
|
||||
vaapi: false,
|
||||
additionalArguments: "",
|
||||
extendedPluginAbilities: false,
|
||||
showExperimentalPluginMenu: false,
|
||||
quickCss: false,
|
||||
};
|
||||
|
||||
|
|
@ -113,6 +122,31 @@ export function getConfig<K extends keyof Settings>(object: K): Settings[K] {
|
|||
configCacheTime = now;
|
||||
return returnData[object];
|
||||
}
|
||||
|
||||
const START_MINIMIZED_MODES = new Set<Settings["startMinimized"]>(["off", "minimized", "tray"]);
|
||||
|
||||
/** Effective startup window mode (CLI overrides are session-only). */
|
||||
export function getStartMinimizedMode(): Settings["startMinimized"] {
|
||||
if (process.argv.includes("--start-in-tray")) return "tray";
|
||||
if (process.argv.includes("--start-minimized")) return "minimized";
|
||||
const mode = getConfig("startMinimized");
|
||||
return START_MINIMIZED_MODES.has(mode) ? mode : "off";
|
||||
}
|
||||
|
||||
/** True when startup should not show the window normally (skip splash). */
|
||||
export function isBackgroundStart(): boolean {
|
||||
return getStartMinimizedMode() !== "off";
|
||||
}
|
||||
|
||||
function migrateStartMinimized(settingsObject: Record<string, unknown>): boolean {
|
||||
const value = settingsObject.startMinimized;
|
||||
if (typeof value === "boolean") {
|
||||
settingsObject.startMinimized = value ? "tray" : "off";
|
||||
console.log(`[Config] Migrated startMinimized boolean → "${settingsObject.startMinimized}"`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): void {
|
||||
const rawData = readFileSync(getConfigLocation(), "utf-8");
|
||||
const parsed = JSON.parse(rawData) as Settings;
|
||||
|
|
@ -175,10 +209,15 @@ export function checkIfConfigExists(): void {
|
|||
export function checkIfConfigIsBroken(): void {
|
||||
try {
|
||||
const settingsData = readFileSync(getConfigLocation(), "utf-8");
|
||||
const settingsObject = JSON.parse(settingsData) as Settings;
|
||||
const settingsObject = JSON.parse(settingsData) as Settings & Record<string, unknown>;
|
||||
|
||||
// Migrate before typeof repair — boolean → "tray" | "off"
|
||||
if (migrateStartMinimized(settingsObject)) {
|
||||
writeFileSync(getConfigLocation(), JSON.stringify(settingsObject, null, 4), "utf-8");
|
||||
}
|
||||
|
||||
// Performance optimization: Update cache after validation
|
||||
configCache = settingsObject;
|
||||
configCache = settingsObject as Settings;
|
||||
configCacheTime = Date.now();
|
||||
|
||||
let configWasFine = true;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
|
||||
import type { Game, GameList } from "arrpc";
|
||||
import { app } from "electron";
|
||||
export function getDetectablesPath() {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const storagePath = path.join(userDataPath, "/storage/");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import type { BrowserWindow } from "electron";
|
||||
|
||||
let scriptCounter = 0;
|
||||
|
||||
export function addStyle(styleUrl: string): void {
|
||||
const id = `legcord-style-${styleUrl.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
if (document.getElementById(id)) return;
|
||||
const style = document.createElement("link");
|
||||
style.id = id;
|
||||
style.rel = "stylesheet";
|
||||
style.type = "text/css";
|
||||
style.href = styleUrl;
|
||||
|
|
@ -9,6 +14,7 @@ export function addStyle(styleUrl: string): void {
|
|||
}
|
||||
|
||||
export function addTheme(id: string, styleString: string): void {
|
||||
if (document.getElementById(id)) return;
|
||||
const style = document.createElement("style");
|
||||
style.textContent = styleString;
|
||||
style.id = id;
|
||||
|
|
@ -16,27 +22,33 @@ export function addTheme(id: string, styleString: string): void {
|
|||
}
|
||||
|
||||
export function addScript(scriptString: string): void {
|
||||
const id = `legcord-script-${++scriptCounter}`;
|
||||
if (document.getElementById(id)) return;
|
||||
const script = document.createElement("script");
|
||||
script.id = id;
|
||||
script.appendChild(document.createTextNode(scriptString));
|
||||
document.body.append(script);
|
||||
}
|
||||
|
||||
export async function injectJS(inject: string): Promise<void> {
|
||||
const id = `legcord-inject-${inject.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
if (document.getElementById(id)) return;
|
||||
const js = await (await fetch(`${inject}`)).text();
|
||||
|
||||
const el = document.createElement("script");
|
||||
|
||||
el.id = id;
|
||||
el.appendChild(document.createTextNode(js));
|
||||
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
||||
export function navigateTo(passedWindow: BrowserWindow, url: string): void {
|
||||
console.log(`[legcord deeplink] Navigating to ${url}`);
|
||||
const safeUrl = JSON.stringify(url);
|
||||
passedWindow.webContents.executeJavaScript(`
|
||||
history.pushState({}, null, ${safeUrl});
|
||||
window.dispatchEvent(new PopStateEvent("popstate", {}));
|
||||
`);
|
||||
// Sanitize: only allow path-like URLs (no protocol, no quotes)
|
||||
const sanitized = url.replace(/[^a-zA-Z0-9/_\-@.]/g, "");
|
||||
console.log(`[legcord deeplink] Navigating to ${sanitized}`);
|
||||
passedWindow.webContents.executeJavaScript(
|
||||
`history.pushState({}, null, ${JSON.stringify(sanitized)});window.dispatchEvent(new PopStateEvent("popstate", {}));`,
|
||||
);
|
||||
passedWindow.setSkipTaskbar(false);
|
||||
if (passedWindow.isMinimized()) passedWindow.restore();
|
||||
passedWindow.show();
|
||||
passedWindow.focus();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { app, powerMonitor } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
|
|
@ -32,16 +32,88 @@ const performance: Preset = {
|
|||
disableFeatures: ["Vulkan"],
|
||||
};
|
||||
|
||||
/** Light GPU boost without forcing the discrete GPU — a middle ground. */
|
||||
const balanced: Preset = {
|
||||
switches: [["enable-gpu-rasterization"], ["enable-zero-copy"], ["ignore-gpu-blocklist"]],
|
||||
enableFeatures: [
|
||||
"CanvasOopRasterization",
|
||||
"UseSkiaRenderer",
|
||||
"WebAssemblyLazyCompilation",
|
||||
"CalculateNativeWinOcclusion",
|
||||
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
|
||||
],
|
||||
disableFeatures: ["Vulkan"],
|
||||
};
|
||||
|
||||
/** Reduce RAM / CPU usage at the cost of some visual smoothness. */
|
||||
const memory: Preset = {
|
||||
switches: [
|
||||
["enable-low-end-device-mode"],
|
||||
["enable-low-res-tiling"],
|
||||
["process-per-site"],
|
||||
["renderer-process-limit", "2"],
|
||||
["force_low_power_gpu"],
|
||||
["disk-cache-size", "67108864"], // 64 MB
|
||||
["skia-resource-cache-limit-mb", "64"],
|
||||
],
|
||||
enableFeatures: ["CalculateNativeWinOcclusion", "TurnOffStreamingMediaCachingOnBattery"],
|
||||
disableFeatures: [],
|
||||
};
|
||||
|
||||
/** Favor voice/video call quality. Platform-specific HW encode is layered on later. */
|
||||
const voip: Preset = {
|
||||
switches: [
|
||||
["enable-gpu-rasterization"],
|
||||
["enable-zero-copy"],
|
||||
["ignore-gpu-blocklist"],
|
||||
["enable-accelerated-video-decode"],
|
||||
["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",
|
||||
"ZeroCopyDesktopCapture",
|
||||
],
|
||||
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
|
||||
};
|
||||
|
||||
/** Minimize input/render latency; keeps the app hot in the background. */
|
||||
const latency: Preset = {
|
||||
switches: [
|
||||
["enable-gpu-rasterization"],
|
||||
["enable-zero-copy"],
|
||||
["ignore-gpu-blocklist"],
|
||||
["force_high_performance_gpu"],
|
||||
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
|
||||
["disable-background-timer-throttling"],
|
||||
["disable-renderer-backgrounding"],
|
||||
["disable-backgrounding-occluded-windows"],
|
||||
["disable-ipc-flooding-protection"],
|
||||
["disable-backing-store-limit"],
|
||||
],
|
||||
enableFeatures: ["EnableDrDc", "CanvasOopRasterization", "UseSkiaRenderer", "WebAssemblyLazyCompilation"],
|
||||
disableFeatures: ["Vulkan"],
|
||||
};
|
||||
|
||||
const smoothExperiment: Preset = {
|
||||
switches: [
|
||||
["enable-gpu-rasterization"],
|
||||
["enable-zero-copy"],
|
||||
["ignore-gpu-blocklist"],
|
||||
["enable-accelerated-video-decode"],
|
||||
["disable-background-timer-throttling"],
|
||||
["disable-renderer-backgrounding"],
|
||||
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
|
||||
["force_high_performance_gpu"],
|
||||
["use-gl", "desktop"],
|
||||
// Do NOT set use-gl=desktop here. On Electron 43+/macOS, Chromium only allows
|
||||
// ANGLE (metal/opengl); use-gl=desktop fails GPU init and then disables all
|
||||
// HW acceleration (including VideoToolbox encode) after repeated crashes.
|
||||
],
|
||||
enableFeatures: [
|
||||
"EnableDrDc",
|
||||
|
|
@ -50,10 +122,11 @@ const smoothExperiment: Preset = {
|
|||
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
|
||||
"UseSkiaRenderer",
|
||||
"WebAssemblyLazyCompilation",
|
||||
"AcceleratedVideoDecodeLinuxGL",
|
||||
"WebRtcHWEncoding",
|
||||
"WebRtcHWDecoding",
|
||||
"AcceleratedVideoEncoder",
|
||||
"AcceleratedVideoDecoder",
|
||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
||||
"ZeroCopyDesktopCapture",
|
||||
],
|
||||
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
|
||||
};
|
||||
|
|
@ -66,27 +139,178 @@ const battery: Preset = {
|
|||
["enable-low-res-tiling"],
|
||||
["process-per-site"],
|
||||
],
|
||||
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery"],
|
||||
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery", "CalculateNativeWinOcclusion"],
|
||||
disableFeatures: [],
|
||||
};
|
||||
|
||||
const vaapi: Preset = {
|
||||
/**
|
||||
* Shared WebRTC / screenshare baseline (no platform encode backend).
|
||||
* Encode is added by macVideoToolbox / winVideoEncode / linux vaapi|software.
|
||||
*/
|
||||
const webrtcHwCommon: Preset = {
|
||||
switches: [
|
||||
["ignore-gpu-blocklist"],
|
||||
["enable-gpu-rasterization"],
|
||||
["enable-zero-copy"],
|
||||
["force_high_performance_gpu"],
|
||||
["use-gl", "desktop"],
|
||||
],
|
||||
enableFeatures: [
|
||||
"AcceleratedVideoDecodeLinuxGL",
|
||||
"AcceleratedVideoEncoder",
|
||||
"AcceleratedVideoDecoder",
|
||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
||||
["enable-accelerated-video-decode"],
|
||||
["enable-gpu-memory-buffer-video-frames"],
|
||||
],
|
||||
enableFeatures: ["WebRtcHWDecoding", "AcceleratedVideoDecoder", "ZeroCopyDesktopCapture", "CanvasOopRasterization"],
|
||||
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
|
||||
};
|
||||
|
||||
/** Windows Media Foundation / Chromium HW encode path. */
|
||||
const winVideoEncode: Preset = {
|
||||
switches: [
|
||||
// Legacy Chromium switches still honored by Electron's WebRTC stack
|
||||
["webrtc-hw-encoding"],
|
||||
["webrtc-hw-decoding"],
|
||||
],
|
||||
enableFeatures: [
|
||||
"WebRtcHWEncoding",
|
||||
"AcceleratedVideoEncoder",
|
||||
// Off by default on Windows; without it CBP (Discord's 42e01f) stays on OpenH264.
|
||||
// SDP munge prefers Baseline, but keep CBP HW as a fallback if negotiation reverts.
|
||||
"PlatformH264CbpEncoding",
|
||||
],
|
||||
disableFeatures: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Linux: force software OpenH264 encode when VAAPI is off.
|
||||
* AMD VCE via VaapiVideoEncodeAccelerator can freeze Discord screenshare for viewers
|
||||
* even when chrome://gpu lists encode profiles.
|
||||
*/
|
||||
const linuxSoftwareVideoEncode: Preset = {
|
||||
switches: [],
|
||||
enableFeatures: [],
|
||||
disableFeatures: ["AcceleratedVideoEncoder", "VaapiVideoEncoder", "WebRtcHWEncoding"],
|
||||
};
|
||||
|
||||
/** macOS VideoToolbox HW encode/decode (Intel + Apple Silicon). */
|
||||
const macVideoToolbox: Preset = {
|
||||
switches: [
|
||||
["ignore-gpu-blocklist"],
|
||||
// After use-gl=desktop / other GPU init failures, Chromium may keep GPU disabled
|
||||
// due to "frequent crashes" even once the bad flag is gone — clear that limit.
|
||||
["disable-gpu-process-crash-limit"],
|
||||
["enable-zero-copy"],
|
||||
["enable-accelerated-video-decode"],
|
||||
// Legacy Chromium switches still honored by Electron's WebRTC stack
|
||||
["webrtc-hw-encoding"],
|
||||
["webrtc-hw-decoding"],
|
||||
["enable-gpu-memory-buffer-video-frames"],
|
||||
],
|
||||
enableFeatures: [
|
||||
"MacosVideoToolbox",
|
||||
"VideoToolboxVideoDecoder",
|
||||
"WebRtcHWEncoding",
|
||||
"WebRtcHWDecoding",
|
||||
"AcceleratedVideoEncoder",
|
||||
"AcceleratedVideoDecoder",
|
||||
"ZeroCopyDesktopCapture",
|
||||
// Helps enumerate platform HW encoders used by WebRTC on Apple GPUs
|
||||
"PlatformHEVCEncoderSupport",
|
||||
],
|
||||
disableFeatures: [],
|
||||
};
|
||||
|
||||
/** Linux VA-API encode/decode (only applied when vaapi setting is on). */
|
||||
const linuxVaapi: Preset = {
|
||||
switches: [
|
||||
["ignore-gpu-blocklist"],
|
||||
["disable-gpu-process-crash-limit"],
|
||||
["enable-gpu-rasterization"],
|
||||
["enable-zero-copy"],
|
||||
["enable-accelerated-video-decode"],
|
||||
["force_high_performance_gpu"],
|
||||
// ANGLE+OpenGL required for AcceleratedVideoDecodeLinuxGL on Wayland.
|
||||
// Do NOT use use-gl=desktop (removed in Electron 43+ / breaks GPU init).
|
||||
["use-gl", "angle"],
|
||||
["use-angle", "gl"],
|
||||
["enable-gpu-memory-buffer-video-frames"],
|
||||
],
|
||||
enableFeatures: [
|
||||
"VaapiIgnoreDriverChecks",
|
||||
"VaapiVideoDecoder",
|
||||
"VaapiVideoEncoder",
|
||||
"AcceleratedVideoEncoder",
|
||||
"AcceleratedVideoDecoder",
|
||||
"AcceleratedVideoDecodeLinuxGL",
|
||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
||||
"WebRtcHWEncoding",
|
||||
"WebRtcHWDecoding",
|
||||
"ZeroCopyDesktopCapture",
|
||||
"CanvasOopRasterization",
|
||||
],
|
||||
// Vulkan is incompatible with ozone wayland and breaks VAAPI GL interop.
|
||||
disableFeatures: ["UseChromeOSDirectVideoDecoder", "Vulkan"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Fedora/RPM Fusion ships H.264/HEVC VA-API in dri-freeworld (patent-encumbered), while
|
||||
* stock /usr/lib64/dri only exposes MPEG2/JPEG. Prefer freeworld so chrome://gpu lists
|
||||
* real encode/decode profiles instead of an empty Video Acceleration Information block.
|
||||
*/
|
||||
export function configureLinuxVaapiEnvironment(): void {
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
const candidates = [
|
||||
"/usr/lib64/dri-freeworld",
|
||||
"/usr/lib64/dri-nonfree",
|
||||
"/usr/lib/dri-freeworld",
|
||||
"/usr/lib/dri-nonfree",
|
||||
];
|
||||
const preferred = candidates.filter((dir) => existsSync(dir));
|
||||
if (preferred.length === 0) return;
|
||||
|
||||
const stock = ["/usr/lib64/dri", "/usr/lib/dri"].filter((dir) => existsSync(dir));
|
||||
const parts = [...preferred, ...stock];
|
||||
const existing = process.env.LIBVA_DRIVERS_PATH;
|
||||
process.env.LIBVA_DRIVERS_PATH = existing ? `${parts.join(":")}:${existing}` : parts.join(":");
|
||||
console.log(`VAAPI: using LIBVA_DRIVERS_PATH=${process.env.LIBVA_DRIVERS_PATH}`);
|
||||
}
|
||||
|
||||
/** Strip Linux encode features that shared presets may have enabled. */
|
||||
function withoutLinuxHwEncode(preset: Preset): Preset {
|
||||
const block = new Set(["AcceleratedVideoEncoder", "VaapiVideoEncoder", "WebRtcHWEncoding"]);
|
||||
return {
|
||||
switches: preset.switches,
|
||||
enableFeatures: preset.enableFeatures.filter((f) => !block.has(f)),
|
||||
disableFeatures: [...new Set([...preset.disableFeatures, ...block])],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the platform-tested WebRTC / screenshare encode+decode stack.
|
||||
* macOS → VideoToolbox; Windows → Chromium HW encode; Linux → VAAPI toggle.
|
||||
*/
|
||||
function applyPlatformVideoStack(base: Preset | undefined): Preset | undefined {
|
||||
if (!getConfig("hardwareAcceleration")) return base;
|
||||
|
||||
const preset = base ? mergePresets(base, webrtcHwCommon) : webrtcHwCommon;
|
||||
console.log("WebRTC HW baseline enabled");
|
||||
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
console.log("macOS VideoToolbox HW encode/decode flags enabled");
|
||||
return mergePresets(preset, macVideoToolbox);
|
||||
case "win32":
|
||||
console.log("Windows HW video encode flags enabled");
|
||||
return mergePresets(preset, winVideoEncode);
|
||||
case "linux":
|
||||
if (getConfig("vaapi")) {
|
||||
console.log("Linux VAAPI HW encode/decode flags enabled");
|
||||
configureLinuxVaapiEnvironment();
|
||||
return mergePresets(preset, linuxVaapi);
|
||||
}
|
||||
console.log("Linux VAAPI off — forcing software WebRTC video encode");
|
||||
return withoutLinuxHwEncode(mergePresets(preset, linuxSoftwareVideoEncode));
|
||||
default:
|
||||
// Other Unix-likes: keep decode baseline; enable generic HW encode.
|
||||
return mergePresets(preset, winVideoEncode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom flags from JSON file in user data directory (cached after first load)
|
||||
* Path:
|
||||
|
|
@ -160,6 +384,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 {
|
||||
// MIT License
|
||||
|
||||
|
|
@ -182,31 +414,58 @@ export function getPreset(): Preset | undefined {
|
|||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
if (getConfig("vaapi")) {
|
||||
console.log("VAAPI mode enabled");
|
||||
mergeWithCustomFlags(vaapi);
|
||||
}
|
||||
let preset: Preset | undefined;
|
||||
|
||||
switch (getConfig("performanceMode")) {
|
||||
case "dynamic":
|
||||
if (powerMonitor.isOnBatteryPower()) {
|
||||
console.log("Battery mode enabled");
|
||||
return mergeWithCustomFlags(battery);
|
||||
preset = battery;
|
||||
} else {
|
||||
console.log("Performance mode enabled");
|
||||
return mergeWithCustomFlags(performance);
|
||||
preset = performance;
|
||||
}
|
||||
break;
|
||||
case "performance":
|
||||
console.log("Performance mode enabled");
|
||||
return mergeWithCustomFlags(performance);
|
||||
preset = performance;
|
||||
break;
|
||||
case "balanced":
|
||||
console.log("Balanced mode enabled");
|
||||
preset = balanced;
|
||||
break;
|
||||
case "battery":
|
||||
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":
|
||||
console.log("Smooth screenshare mode enabled");
|
||||
return mergeWithCustomFlags(smoothExperiment);
|
||||
preset = smoothExperiment;
|
||||
break;
|
||||
default:
|
||||
console.log("No performance modes set");
|
||||
}
|
||||
|
||||
// Platform-specific video encode/decode (macOS VideoToolbox / Win HW / Linux VAAPI).
|
||||
// Shared voip/smoothScreenshare presets must not carry Linux-only or cross-platform
|
||||
// encode flags that would undermine the macOS stack we just fixed.
|
||||
preset = applyPlatformVideoStack(preset);
|
||||
|
||||
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("");
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import path from "node:path";
|
|||
import { app, shell } from "electron";
|
||||
import type { Keybind } from "../@types/keybind.js";
|
||||
import { mainWindows } from "../discord/window.js";
|
||||
import { navigateTo } from "./dom.js";
|
||||
|
||||
let isAudioEngineEnabled = false;
|
||||
|
||||
export function runAction(keybind: Keybind) {
|
||||
|
|
@ -27,6 +29,9 @@ export function runAction(keybind: Keybind) {
|
|||
case "openQuickCss":
|
||||
openQuickCss();
|
||||
break;
|
||||
case "openSettings":
|
||||
openSettings();
|
||||
break;
|
||||
case "runJavascript":
|
||||
if (!keybind.js) break;
|
||||
runJavascript(keybind.js);
|
||||
|
|
@ -122,3 +127,10 @@ function runJavascript(js: string) {
|
|||
window.webContents.executeJavaScript(js);
|
||||
});
|
||||
}
|
||||
|
||||
export function openSettings() {
|
||||
// won't load the correct page anyway (will just do /account) cause shelter doesn't hijack discord's routing
|
||||
mainWindows.forEach((window) => {
|
||||
navigateTo(window, "/settings/legcord-settings");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
}
|
||||
88
src/common/sanitization.ts
Normal file
88
src/common/sanitization.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/** True when hostname is exactly `domain` or a subdomain of it (case-insensitive). */
|
||||
export function hostnameMatches(hostname: string, domain: string): boolean {
|
||||
const host = hostname.toLowerCase();
|
||||
const target = domain.toLowerCase();
|
||||
return host === target || host.endsWith(`.${target}`);
|
||||
}
|
||||
|
||||
export function tryParseUrl(urlString: string): URL | null {
|
||||
try {
|
||||
return new URL(urlString);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when the frame is a YouTube embed (or Discord Activities YouTube proxy). */
|
||||
export function isYouTubeEmbedOrProxyFrame(frameUrl: string): boolean {
|
||||
const url = tryParseUrl(frameUrl);
|
||||
if (!url) return false;
|
||||
|
||||
if (hostnameMatches(url.hostname, "youtube.com") && url.pathname.includes("/embed/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Discord Activities may proxy YouTube through *.discordsays.com (youtube.com appears in the path/query)
|
||||
if (
|
||||
hostnameMatches(url.hostname, "discordsays.com") &&
|
||||
(url.pathname.includes("youtube.com") || url.search.includes("youtube.com"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Telemetry / noise hosts and Discord science endpoints we cancel in webRequest. */
|
||||
export function isTelemetryBlockedUrl(requestUrl: string): boolean {
|
||||
const url = tryParseUrl(requestUrl);
|
||||
if (url?.protocol !== "https:") return false;
|
||||
|
||||
if (/^\/api\/v\d+\/science(?:\/|$)/.test(url.pathname)) return true;
|
||||
if (hostnameMatches(url.hostname, "sentry.io")) return true;
|
||||
if (hostnameMatches(url.hostname, "nel.cloudflare.com")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blob downloads for Discord calendar (.ics) use `blob:https://discord.com/<uuid>`.
|
||||
* Parse the blob payload URL so `discord.com.evil` / userinfo tricks cannot match.
|
||||
*/
|
||||
export function isDiscordIcsBlobUrl(urlString: string): boolean {
|
||||
const blobUrl = tryParseUrl(urlString);
|
||||
if (blobUrl?.protocol !== "blob:") return false;
|
||||
|
||||
const inner = tryParseUrl(blobUrl.pathname);
|
||||
if (!inner || (inner.protocol !== "https:" && inner.protocol !== "http:")) return false;
|
||||
|
||||
return hostnameMatches(inner.hostname, "discord.com");
|
||||
}
|
||||
|
||||
/** Discord stream popout windows (stable / canary / PTB). */
|
||||
export function isDiscordPopoutUrl(urlString: string): boolean {
|
||||
const url = tryParseUrl(urlString);
|
||||
if (url?.protocol !== "https:" || url.pathname !== "/popout") return false;
|
||||
|
||||
const host = url.hostname.toLowerCase();
|
||||
return host === "discord.com" || host === "canary.discord.com" || host === "ptb.discord.com";
|
||||
}
|
||||
|
||||
const DEFAULT_ALLOWED_LOCALHOST_WS_PORTS = new Set([1211, 1112, 6888]);
|
||||
|
||||
/**
|
||||
* Block stray localhost WebSocket probes, except known Legcord/local RPC ports.
|
||||
* Uses URL parsing instead of substring checks on the raw request URL.
|
||||
*/
|
||||
export function isBlockedLocalhostWebSocket(
|
||||
requestUrl: string,
|
||||
allowedPorts: ReadonlySet<number> = DEFAULT_ALLOWED_LOCALHOST_WS_PORTS,
|
||||
): boolean {
|
||||
const url = tryParseUrl(requestUrl);
|
||||
if (!url || (url.protocol !== "ws:" && url.protocol !== "wss:")) return false;
|
||||
if (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") return false;
|
||||
|
||||
const port = url.port ? Number(url.port) : url.protocol === "wss:" ? 443 : 80;
|
||||
if (!Number.isFinite(port)) return true;
|
||||
return !allowedPorts.has(port);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { type BrowserWindow, app } from "electron";
|
||||
import { app, type BrowserWindow } from "electron";
|
||||
import type { ThemeManifest } from "../@types/themeManifest.js";
|
||||
import { mainWindows } from "../discord/window.js";
|
||||
import { getConfig } from "./config.js";
|
||||
|
|
@ -112,6 +112,111 @@ function getThemeManifest(themeId: string): ThemeManifest | null {
|
|||
}
|
||||
}
|
||||
|
||||
// Performance optimization: Cached theme list with directory watcher
|
||||
const THEME_LIST_CACHE_TTL = 2000;
|
||||
let themeListCache: ThemeManifest[] | null = null;
|
||||
let themeListCacheTime = 0;
|
||||
let themeWatcher: fs.FSWatcher | null = null;
|
||||
let themeListRefreshTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
function importLooseThemeFiles(): void {
|
||||
try {
|
||||
if (!fs.existsSync(themesFolder)) return;
|
||||
const entries = fs.readdirSync(themesFolder);
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(themesFolder, entry);
|
||||
const stat = fs.statSync(entryPath);
|
||||
if (stat.isFile() && (entry.endsWith(".css") || entry.endsWith(".theme.css"))) {
|
||||
const code = fs.readFileSync(entryPath, "utf8");
|
||||
installThemeFromCode(code);
|
||||
try {
|
||||
fs.unlinkSync(entryPath);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Theme Manager] Failed to import loose theme files:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshThemeListCache(): ThemeManifest[] {
|
||||
try {
|
||||
if (!fs.existsSync(themesFolder)) {
|
||||
themeListCache = [];
|
||||
themeListCacheTime = Date.now();
|
||||
return themeListCache;
|
||||
}
|
||||
const themes: ThemeManifest[] = [];
|
||||
const entries = fs.readdirSync(themesFolder);
|
||||
for (const entry of entries) {
|
||||
const manifestPath = path.join(themesFolder, entry, "manifest.json");
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
const manifest = getThemeManifest(entry);
|
||||
if (manifest) {
|
||||
themes.push({ ...manifest, id: entry });
|
||||
}
|
||||
}
|
||||
}
|
||||
themeListCache = themes;
|
||||
themeListCacheTime = Date.now();
|
||||
return themes;
|
||||
} catch (err) {
|
||||
console.error("[Theme Manager] Failed to refresh theme list cache:", err);
|
||||
return themeListCache ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedThemeList(): ThemeManifest[] {
|
||||
const now = Date.now();
|
||||
if (themeListCache && now - themeListCacheTime < THEME_LIST_CACHE_TTL) {
|
||||
return themeListCache;
|
||||
}
|
||||
importLooseThemeFiles();
|
||||
return refreshThemeListCache();
|
||||
}
|
||||
|
||||
export function invalidateThemeListCache(): void {
|
||||
themeListCache = null;
|
||||
}
|
||||
|
||||
function debouncedRefreshThemeList(): void {
|
||||
if (themeListRefreshTimeout) {
|
||||
clearTimeout(themeListRefreshTimeout);
|
||||
}
|
||||
themeListRefreshTimeout = setTimeout(() => {
|
||||
refreshThemeListCache();
|
||||
themeListRefreshTimeout = null;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
export function startThemeWatcher(): void {
|
||||
if (themeWatcher) return;
|
||||
try {
|
||||
if (!fs.existsSync(themesFolder)) {
|
||||
fs.mkdirSync(themesFolder, { recursive: true });
|
||||
}
|
||||
themeWatcher = fs.watch(themesFolder, { recursive: true }, (eventType, filename) => {
|
||||
if (filename && (filename.endsWith("manifest.json") || eventType === "rename")) {
|
||||
debouncedRefreshThemeList();
|
||||
}
|
||||
});
|
||||
refreshThemeListCache();
|
||||
} catch (err) {
|
||||
console.error("[Theme Manager] Failed to start theme watcher:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export function stopThemeWatcher(): void {
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = null;
|
||||
}
|
||||
if (themeListRefreshTimeout) {
|
||||
clearTimeout(themeListRefreshTimeout);
|
||||
themeListRefreshTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function injectThemesMain(browserWindow: BrowserWindow): void {
|
||||
if (process.argv.includes("--safe-mode")) return;
|
||||
if (!fs.existsSync(themesFolder)) {
|
||||
|
|
@ -123,11 +228,13 @@ export function injectThemesMain(browserWindow: BrowserWindow): void {
|
|||
const files = fs.readdirSync(themesFolder);
|
||||
for (const file of files) {
|
||||
const themePath = path.join(themesFolder, file);
|
||||
if (fs.statSync(themePath).isFile() && file.endsWith(".DS_Store")) {
|
||||
if (fs.statSync(themePath).isFile() && (file.endsWith(".css") || file.endsWith(".theme.css"))) {
|
||||
console.log(`[Theme Manager] Local theme detected: ${themePath}`);
|
||||
installTheme(themePath).then(() => {
|
||||
const code = fs.readFileSync(themePath, "utf8");
|
||||
installThemeFromCode(code);
|
||||
try {
|
||||
fs.unlinkSync(themePath);
|
||||
});
|
||||
} catch {}
|
||||
} else {
|
||||
try {
|
||||
const themeFile = getThemeManifest(file);
|
||||
|
|
@ -168,10 +275,11 @@ export function uninstallTheme(id: string) {
|
|||
fs.rmdirSync(path.join(themesFolder, `${id}-BD`), { recursive: true });
|
||||
console.log(`Removed ${id} folder`);
|
||||
}
|
||||
themeManifestCache.delete(id);
|
||||
invalidateThemeListCache();
|
||||
}
|
||||
|
||||
export function setThemeEnabled(id: string, enabled: boolean) {
|
||||
// Performance optimization: Use cached manifest if available
|
||||
let manifest = getThemeManifest(id);
|
||||
if (!manifest) {
|
||||
manifest = JSON.parse(fs.readFileSync(path.join(themesFolder, id, "/manifest.json"), "utf8")) as ThemeManifest;
|
||||
|
|
@ -190,13 +298,31 @@ export function setThemeEnabled(id: string, enabled: boolean) {
|
|||
passedWindow.webContents.send("removeTheme", id);
|
||||
console.log(`[Theme Manager] Removing ${manifest.name} made by ${manifest.author}`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
manifest.enabled = enabled;
|
||||
fs.writeFileSync(`${themesFolder}/${id}/manifest.json`, JSON.stringify(manifest));
|
||||
|
||||
// Performance optimization: Invalidate cache
|
||||
themeManifestCache.delete(id);
|
||||
invalidateThemeListCache();
|
||||
}
|
||||
|
||||
function installThemeFromCode(code: string, linkOrPath?: string): void {
|
||||
const manifest = parseBDManifest(code);
|
||||
const themePath = path.join(themesFolder, `${manifest.name?.replace(" ", "-")}-BD`);
|
||||
if (!fs.existsSync(themePath)) {
|
||||
fs.mkdirSync(themePath);
|
||||
console.log(`Created ${manifest.name} folder`);
|
||||
}
|
||||
if (linkOrPath && manifest.updateSrc === undefined) {
|
||||
manifest.updateSrc = linkOrPath;
|
||||
}
|
||||
if (code.includes(".titlebar")) manifest.supportsLegcordTitlebar = true;
|
||||
else manifest.supportsLegcordTitlebar = false;
|
||||
fs.writeFileSync(path.join(themePath, "manifest.json"), JSON.stringify(manifest));
|
||||
fs.writeFileSync(path.join(themePath, "src.css"), code);
|
||||
invalidateThemeListCache();
|
||||
}
|
||||
|
||||
export async function installTheme(linkOrPath: string) {
|
||||
|
|
@ -208,19 +334,7 @@ export async function installTheme(linkOrPath: string) {
|
|||
} else {
|
||||
code = fs.readFileSync(linkOrPath, "utf8");
|
||||
}
|
||||
const manifest = parseBDManifest(code);
|
||||
const themePath = path.join(themesFolder, `${manifest.name?.replace(" ", "-")}-BD`);
|
||||
if (!fs.existsSync(themePath)) {
|
||||
fs.mkdirSync(themePath);
|
||||
console.log(`Created ${manifest.name} folder`);
|
||||
}
|
||||
if (isLinkImport && manifest.updateSrc === undefined) {
|
||||
manifest.updateSrc = linkOrPath;
|
||||
}
|
||||
if (code.includes(".titlebar")) manifest.supportsLegcordTitlebar = true;
|
||||
else manifest.supportsLegcordTitlebar = false;
|
||||
fs.writeFileSync(path.join(themePath, "manifest.json"), JSON.stringify(manifest));
|
||||
fs.writeFileSync(path.join(themePath, "src.css"), code);
|
||||
installThemeFromCode(code, isLinkImport ? linkOrPath : undefined);
|
||||
}
|
||||
|
||||
export function initQuickCss(browserWindow: BrowserWindow) {
|
||||
|
|
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import type { WindowState } from "../@types/windowState.js";
|
|||
// Performance optimization: Cache window state to avoid reading file on every call
|
||||
let windowStateCache: WindowState | null = null;
|
||||
let windowStateCacheTime = 0;
|
||||
const WINDOW_STATE_CACHE_TTL = 1000; // Cache for 1 second
|
||||
const WINDOW_STATE_CACHE_TTL = 5000; // Cache for 5 seconds
|
||||
|
||||
export function getWindowStateLocation() {
|
||||
const userDataPath = app.getPath("userData");
|
||||
|
|
|
|||
37
src/common/windowVisibility.ts
Normal file
37
src/common/windowVisibility.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { BrowserWindow } from "electron";
|
||||
import { getConfig, getStartMinimizedMode } from "./config.js";
|
||||
|
||||
/** Show the main window and restore taskbar/dock presence after a tray-only start. */
|
||||
export function revealWindow(win: BrowserWindow): void {
|
||||
if (win.isDestroyed()) return;
|
||||
win.setSkipTaskbar(false);
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.show();
|
||||
win.focus();
|
||||
}
|
||||
|
||||
/** Apply startMinimized mode when splash will not call splashEnd, or from splashEnd itself. */
|
||||
export function applyStartupWindowVisibility(win: BrowserWindow): void {
|
||||
if (win.isDestroyed()) return;
|
||||
const mode = getStartMinimizedMode();
|
||||
switch (mode) {
|
||||
case "minimized":
|
||||
win.setSkipTaskbar(false);
|
||||
win.show();
|
||||
win.minimize();
|
||||
break;
|
||||
case "tray":
|
||||
if (getConfig("tray") === "disabled") {
|
||||
console.warn(
|
||||
'[Window] startMinimized is "tray" but the tray icon is disabled; the window will be hidden with no tray.',
|
||||
);
|
||||
}
|
||||
win.setSkipTaskbar(true);
|
||||
win.hide();
|
||||
break;
|
||||
default:
|
||||
win.setSkipTaskbar(false);
|
||||
win.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Legcord Quick CSS Editor</title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<link rel="stylesheet" href="legcord://assets/css/editor.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="legcord://assets/js/monacoLoader.js"></script>
|
||||
<script type="text/javascript" src="./monaco/vs/loader.js"></script>
|
||||
<div id="editorCode"></div>
|
||||
<script>
|
||||
const cssCode = cssEditor.get;
|
||||
const monacoRoot = new URL("./monaco/", window.location.href).href;
|
||||
const vs = new URL("./monaco/vs", window.location.href).href;
|
||||
|
||||
require.config({
|
||||
paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor/min/vs" }
|
||||
paths: { vs }
|
||||
});
|
||||
|
||||
window.MonacoEnvironment = {
|
||||
getWorkerUrl: function(workerId, label) {
|
||||
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
||||
self.MonacoEnvironment = {
|
||||
baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/'
|
||||
};
|
||||
importScripts('https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/base/worker/workerMain.js');`)}`;
|
||||
}
|
||||
getWorkerUrl: () =>
|
||||
`data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
||||
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoRoot)} };
|
||||
importScripts(${JSON.stringify(`${vs}/base/worker/workerMain.js`)});
|
||||
`)}`,
|
||||
};
|
||||
|
||||
// Monaco init
|
||||
require(["vs/editor/editor.main"], function() {
|
||||
require(["vs/editor/editor.main"], () => {
|
||||
createEditor(editorCode);
|
||||
});
|
||||
|
||||
function createEditor(editorContainer) {
|
||||
let editor = monaco.editor.create(editorContainer, {
|
||||
const editor = monaco.editor.create(editorContainer, {
|
||||
value: cssCode,
|
||||
language: "css",
|
||||
minimap: { enabled: false },
|
||||
|
|
@ -51,8 +52,6 @@
|
|||
cssEditor.set(editor.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -14,7 +14,7 @@ export function openCssEditor(file: string) {
|
|||
preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"),
|
||||
},
|
||||
});
|
||||
cssWindow.loadURL(`file://${import.meta.dirname}/html/editor.html`);
|
||||
void cssWindow.loadFile(path.join(import.meta.dirname, "html", "editor.html"));
|
||||
|
||||
ipcMain.on("editor-setCSS", (_event, css: string) => {
|
||||
fs.writeFileSync(file, css);
|
||||
|
|
|
|||
186
src/dbus.ts
Normal file
186
src/dbus.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
interface as dbusInterface,
|
||||
type MessageBus,
|
||||
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";
|
||||
|
||||
const isLinux = process.platform === "linux";
|
||||
|
||||
// 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);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let bus: MessageBus | undefined;
|
||||
let legcordInterface: LegcordInterface | undefined;
|
||||
|
||||
function getBus(): MessageBus {
|
||||
if (!bus) {
|
||||
bus = sessionBus();
|
||||
legcordInterface = new LegcordInterface();
|
||||
}
|
||||
return bus;
|
||||
}
|
||||
|
||||
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 getBus()
|
||||
.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() {
|
||||
if (!isLinux) return;
|
||||
|
||||
const portalObj = await getBus().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 getBus()
|
||||
.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> {
|
||||
if (!isLinux) return;
|
||||
|
||||
const dbus = getBus();
|
||||
await dbus.requestName(DBUS_INTERFACE_NAME);
|
||||
dbus.export(DBUS_ADDRESS, legcordInterface!);
|
||||
console.info(`Registered DBus service at ${DBUS_INTERFACE_NAME} ${DBUS_ADDRESS}`);
|
||||
|
||||
// console.debug(legcordInterface)
|
||||
}
|
||||
|
||||
export function disconnectDbusService(): void {
|
||||
if (!bus) return;
|
||||
bus.disconnect();
|
||||
bus = undefined;
|
||||
legcordInterface = undefined;
|
||||
}
|
||||
|
|
@ -54,8 +54,7 @@ async function cacheCheck(mod: ValidMods) {
|
|||
}
|
||||
try {
|
||||
const latestRef = await getRef(modData[mod].repoData);
|
||||
// biome-ignore lint/correctness/noConstantCondition: https://github.com/Legcord/Legcord/issues/763
|
||||
if (/*latestRef === modCache![mod]*/ false) {
|
||||
if (latestRef === modCache![mod]) {
|
||||
console.log(`[Mod Loader]: ${mod} Cache hit!`);
|
||||
return;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { platform } from "node:os";
|
||||
import { app, session } from "electron";
|
||||
const pluginFolder = `${app.getPath("userData")}/plugins`;
|
||||
|
||||
const extensionFolder = `${app.getPath("userData")}/extensions`;
|
||||
|
||||
let prefix = "";
|
||||
|
||||
if (!existsSync(pluginFolder)) {
|
||||
mkdirSync(pluginFolder);
|
||||
console.log("Created missing plugin folder");
|
||||
if (!existsSync(extensionFolder)) {
|
||||
mkdirSync(extensionFolder);
|
||||
console.log("Created missing extensions folder");
|
||||
}
|
||||
await app.whenReady().then(() => {
|
||||
readdirSync(pluginFolder).forEach(async (file) => {
|
||||
readdirSync(extensionFolder).forEach(async (file) => {
|
||||
try {
|
||||
// NOTE - The below type assertion is just what we need from the chrome manifest
|
||||
if (platform() === "win32") prefix = "file://";
|
||||
const manifest = (await import(`${prefix}${pluginFolder}/${file}/manifest.json`, {
|
||||
const manifest = (await import(`${prefix}${extensionFolder}/${file}/manifest.json`, {
|
||||
with: { type: "json" },
|
||||
})) as { name: string; author: string; type: "json" };
|
||||
|
||||
void session.defaultSession.loadExtension(`${pluginFolder}/${file}`); // NOTE - Awaiting this will cause plugins to not inject
|
||||
void session.defaultSession.loadExtension(`${extensionFolder}/${file}`); // NOTE - Awaiting this will cause plugins to not inject
|
||||
console.log(`[Mod loader] Loaded ${manifest.name} made by ${manifest.author}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function registerGlobalKeybinds() {
|
|||
app.on("will-quit", () => {
|
||||
try {
|
||||
globalShortcut.unregisterAll();
|
||||
} catch (e) {}
|
||||
} catch (_e) {}
|
||||
});
|
||||
|
||||
export function refreshGlobalKeybinds() {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Game } from "arrpc";
|
||||
import { type BrowserWindow, app, clipboard, dialog, ipcMain, shell } from "electron";
|
||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import type { Keybind } from "../@types/keybind.js";
|
||||
import type { Settings } from "../@types/settings.js";
|
||||
import type { ThemeManifest } from "../@types/themeManifest.js";
|
||||
import {
|
||||
type BackupSavePayload,
|
||||
applyBackupFromMap,
|
||||
type BackupSavePayload,
|
||||
buildBackupZipBuffer,
|
||||
readBackupZipToMap,
|
||||
} from "../common/backup.js";
|
||||
|
|
@ -21,20 +21,32 @@ import {
|
|||
import { getConfig, getConfigLocation, setConfig, setConfigBulk } from "../common/config.js";
|
||||
import { addDetectable, getDetectables, removeDetectable } from "../common/detectables.js";
|
||||
import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js";
|
||||
import { disableQuickCss, initQuickCss, installTheme, setThemeEnabled, uninstallTheme } from "../common/themes.js";
|
||||
import {
|
||||
disableQuickCss,
|
||||
getCachedThemeList,
|
||||
initQuickCss,
|
||||
installTheme,
|
||||
invalidateThemeListCache,
|
||||
setThemeEnabled,
|
||||
startThemeWatcher,
|
||||
uninstallTheme,
|
||||
} from "../common/themes.js";
|
||||
import { getDisplayVersion, getVersion } from "../common/version.js";
|
||||
import { applyStartupWindowVisibility, revealWindow } from "../common/windowVisibility.js";
|
||||
import { openCssEditor } from "../cssEditor/main.js";
|
||||
import { getAppliedFlags, handleRestart } from "../main.js";
|
||||
import { isPowerSavingEnabled, setPowerSaving } from "../power.js";
|
||||
import constPaths from "../shared/consts/paths.js";
|
||||
import { splashWindow } from "../splash/main.js";
|
||||
import { refreshGlobalKeybinds } from "./globalKeybinds.js";
|
||||
import { getRuntimeEntries, getRuntimeScript, listPlugins, reloadPlugin, setPluginEnabled } from "./plugins/manager.js";
|
||||
import { processList, refreshProcessList } from "./rpcProcess.js";
|
||||
import { importGuilds, mainTouchBar, setVoiceState, voiceTouchBar } from "./touchbar.js";
|
||||
|
||||
const userDataPath = app.getPath("userData");
|
||||
const storagePath = path.join(userDataPath, "/storage/");
|
||||
const themesPath = path.join(userDataPath, "/themes/");
|
||||
const extensionsPath = path.join(userDataPath, "/extensions/");
|
||||
const pluginsPath = path.join(userDataPath, "/plugins/");
|
||||
const pluginStoragePath = path.join(userDataPath, "/plugin-storage/");
|
||||
const quickCssPath = path.join(userDataPath, "/quickCss.css");
|
||||
|
|
@ -58,7 +70,42 @@ function ifExistsRead(path: string): string | undefined {
|
|||
if (existsSync(path)) return readFileSync(path, "utf-8");
|
||||
}
|
||||
|
||||
let ipcRegistered = false;
|
||||
|
||||
const chromeInternalsWindows = new Map<string, BrowserWindow>();
|
||||
|
||||
function openChromeInternalsPage(url: "chrome://webrtc-internals/" | "chrome://gpu/", title: string): void {
|
||||
const existing = chromeInternalsWindows.get(url);
|
||||
if (existing && !existing.isDestroyed()) {
|
||||
if (existing.isMinimized()) existing.restore();
|
||||
existing.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1100,
|
||||
height: 800,
|
||||
minWidth: 640,
|
||||
minHeight: 480,
|
||||
title,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
void win.loadURL(url);
|
||||
chromeInternalsWindows.set(url, win);
|
||||
win.on("closed", () => {
|
||||
chromeInternalsWindows.delete(url);
|
||||
});
|
||||
}
|
||||
|
||||
export function registerIpc(passedWindow: BrowserWindow): void {
|
||||
if (ipcRegistered) return;
|
||||
ipcRegistered = true;
|
||||
startThemeWatcher();
|
||||
ipcMain.handle("getShelterBundle", () => {
|
||||
return {
|
||||
js: ifExistsRead(path.join(app.getPath("userData"), "shelter.js")),
|
||||
|
|
@ -81,13 +128,11 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
});
|
||||
ipcMain.handle("getCustomBundle", () => {
|
||||
const enabled = getConfig("mods").includes("custom");
|
||||
if (enabled) {
|
||||
return {
|
||||
js: ifExistsRead(path.join(app.getPath("userData"), "custom.js")),
|
||||
css: ifExistsRead(path.join(app.getPath("userData"), "custom.css")),
|
||||
js: enabled ? ifExistsRead(path.join(app.getPath("userData"), "custom.js")) : undefined,
|
||||
css: enabled ? ifExistsRead(path.join(app.getPath("userData"), "custom.css")) : undefined,
|
||||
enabled,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// theming
|
||||
|
|
@ -165,26 +210,16 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
});
|
||||
|
||||
ipcMain.on("getThemes", (event) => {
|
||||
const themes = [];
|
||||
const themeFolders = readdirSync(themesPath);
|
||||
for (const folder of themeFolders) {
|
||||
if (existsSync(`${themesPath}/${folder}/manifest.json`)) {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(`${themesPath}/${folder}/manifest.json`, "utf8"),
|
||||
) as ThemeManifest;
|
||||
themes.push({ ...manifest, id: folder });
|
||||
}
|
||||
}
|
||||
event.returnValue = themes;
|
||||
event.returnValue = getCachedThemeList();
|
||||
});
|
||||
ipcMain.on("refreshThemesCache", (event) => {
|
||||
invalidateThemeListCache();
|
||||
event.returnValue = getCachedThemeList();
|
||||
});
|
||||
|
||||
ipcMain.on("splashEnd", () => {
|
||||
splashWindow.close();
|
||||
if (getConfig("startMinimized")) {
|
||||
passedWindow.hide();
|
||||
} else {
|
||||
passedWindow.show();
|
||||
}
|
||||
splashWindow?.close();
|
||||
applyStartupWindowVisibility(passedWindow);
|
||||
});
|
||||
ipcMain.on("setLang", (_event, lang: string) => {
|
||||
setLang(lang);
|
||||
|
|
@ -232,7 +267,7 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
passedWindow.unmaximize();
|
||||
});
|
||||
ipcMain.on("win-show", () => {
|
||||
passedWindow.show();
|
||||
revealWindow(passedWindow);
|
||||
});
|
||||
ipcMain.on("win-hide", () => {
|
||||
passedWindow.hide();
|
||||
|
|
@ -316,6 +351,9 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
shell.showItemInFolder(themesPath);
|
||||
});
|
||||
ipcMain.on("openPluginsFolder", () => {
|
||||
shell.showItemInFolder(extensionsPath);
|
||||
});
|
||||
ipcMain.on("openRuntimePluginsFolder", () => {
|
||||
shell.showItemInFolder(pluginsPath);
|
||||
});
|
||||
ipcMain.on("openCrashesFolder", () => {
|
||||
|
|
@ -344,6 +382,12 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
ipcMain.on("copyGPUInfo", () => {
|
||||
clipboard.writeText(JSON.stringify(app.getGPUFeatureStatus()));
|
||||
});
|
||||
ipcMain.on("openWebRTCInternals", () => {
|
||||
openChromeInternalsPage("chrome://webrtc-internals/", "WebRTC Internals");
|
||||
});
|
||||
ipcMain.on("openGPUInfo", () => {
|
||||
openChromeInternalsPage("chrome://gpu/", "GPU");
|
||||
});
|
||||
ipcMain.on("openCustomIconDialog", () => {
|
||||
dialog
|
||||
.showOpenDialog({
|
||||
|
|
@ -367,6 +411,25 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
ipcMain.on("getProcessList", (event) => {
|
||||
event.returnValue = processList;
|
||||
});
|
||||
ipcMain.handle("plugins:list", () => listPlugins());
|
||||
ipcMain.handle("plugins:set-enabled", async (_event, pluginId: string, enabled: boolean) => {
|
||||
if (typeof pluginId !== "string" || typeof enabled !== "boolean") {
|
||||
return { ok: false };
|
||||
}
|
||||
return { ok: await setPluginEnabled(pluginId, enabled) };
|
||||
});
|
||||
ipcMain.handle("plugins:reload", async (_event, pluginId: string) => {
|
||||
if (typeof pluginId !== "string") return { ok: false };
|
||||
return { ok: await reloadPlugin(pluginId) };
|
||||
});
|
||||
ipcMain.handle("plugins:get-runtime-entries", (_event, target: "preload" | "renderer") => {
|
||||
if (target !== "preload" && target !== "renderer") return [];
|
||||
return getRuntimeEntries(target);
|
||||
});
|
||||
ipcMain.handle("plugins:get-runtime-script", (_event, pluginId: string, target: "preload" | "renderer") => {
|
||||
if (typeof pluginId !== "string" || (target !== "preload" && target !== "renderer")) return null;
|
||||
return getRuntimeScript(pluginId, target);
|
||||
});
|
||||
|
||||
// custom detectables control
|
||||
ipcMain.on("refreshProcessList", () => {
|
||||
|
|
@ -451,6 +514,7 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
const zipBuf = buildBackupZipBuffer(payload, {
|
||||
userDataPath,
|
||||
themesPath,
|
||||
extensionsPath,
|
||||
pluginsPath,
|
||||
pluginStoragePath,
|
||||
quickCssPath,
|
||||
|
|
@ -491,6 +555,7 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
const { clientMods } = applyBackupFromMap(map, {
|
||||
userDataPath,
|
||||
themesPath,
|
||||
extensionsPath,
|
||||
pluginsPath,
|
||||
pluginStoragePath,
|
||||
quickCssPath,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { BrowserWindow, Menu, type MenuItemConstructorOptions, app } from "electron";
|
||||
import { app, BrowserWindow, Menu, type MenuItemConstructorOptions } from "electron";
|
||||
import type { Keybind, KeybindActions } from "../@types/keybind.js";
|
||||
import { getConfig } from "../common/config.js";
|
||||
import { setForceQuit } from "../common/forceQuit.js";
|
||||
import { runAction } from "../common/keybindActions.js";
|
||||
import { openSettings, runAction } from "../common/keybindActions.js";
|
||||
import { getLang } from "../common/lang.js";
|
||||
import { revealWindow } from "../common/windowVisibility.js";
|
||||
import { mainWindows } from "./window.js";
|
||||
|
||||
const keybindActionLabels: Record<KeybindActions, string> = {
|
||||
|
|
@ -15,6 +16,7 @@ const keybindActionLabels: Record<KeybindActions, string> = {
|
|||
navigateBack: "keybind-navigateBack",
|
||||
runJavascript: "keybind-runJavascript",
|
||||
openQuickCss: "keybind-openQuickCss",
|
||||
openSettings: "keybind-openSettings",
|
||||
};
|
||||
|
||||
export function setMenu(): void {
|
||||
|
|
@ -52,17 +54,8 @@ export function setMenu(): void {
|
|||
accelerator: "Cmd+,",
|
||||
click() {
|
||||
mainWindows.forEach((mainWindow) => {
|
||||
mainWindow.show();
|
||||
void mainWindow.webContents.executeJavaScript(`window.shelter.flux.dispatcher.dispatch({
|
||||
"type": "USER_SETTINGS_MODAL_OPEN",
|
||||
"section": "legcord-settings",
|
||||
"subsection": null,
|
||||
"openWithoutBackstack": false
|
||||
})`);
|
||||
void mainWindow.webContents.executeJavaScript(
|
||||
`window.shelter.flux.dispatcher.dispatch({type: "LAYER_PUSH", component: "USER_SETTINGS"})`,
|
||||
);
|
||||
// this opens the legcord tab directly in the settings modal
|
||||
revealWindow(mainWindow);
|
||||
openSettings();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
|||
310
src/discord/plugins/manager.ts
Normal file
310
src/discord/plugins/manager.ts
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
|
||||
import { after, before, instead } from "spitroast/dist/index.mjs";
|
||||
import { getConfig, setConfig } from "../../common/config.js";
|
||||
|
||||
type PluginTarget = "main" | "preload" | "renderer";
|
||||
type Cleanup = () => void;
|
||||
|
||||
interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
main?: string;
|
||||
preload?: string;
|
||||
renderer?: string;
|
||||
compatibleVersions?: string[];
|
||||
}
|
||||
|
||||
interface PluginRecord {
|
||||
manifest: PluginManifest;
|
||||
directory: string;
|
||||
enabled: boolean;
|
||||
compatible: boolean;
|
||||
compatibilityMessage?: string;
|
||||
loadedMain: boolean;
|
||||
cleanups: Cleanup[];
|
||||
}
|
||||
|
||||
interface PluginMainApi {
|
||||
id: string;
|
||||
manifest: PluginManifest;
|
||||
logger: Pick<Console, "log" | "warn" | "error">;
|
||||
patcher: {
|
||||
before: typeof before;
|
||||
after: typeof after;
|
||||
instead: typeof instead;
|
||||
};
|
||||
electron: {
|
||||
app: typeof app;
|
||||
BrowserWindow: typeof BrowserWindow;
|
||||
ipcMain: typeof ipcMain;
|
||||
dialog: typeof dialog;
|
||||
shell: typeof shell;
|
||||
};
|
||||
onCleanup: (cleanup: Cleanup) => void;
|
||||
}
|
||||
|
||||
const pluginFolder = path.join(app.getPath("userData"), "/plugins");
|
||||
const currentLegcordVersion = app.getVersion();
|
||||
const records = new Map<string, PluginRecord>();
|
||||
const VALID_PLUGIN_ID = /^[a-zA-Z0-9._-]{1,64}$/;
|
||||
const VALID_ENTRY_PATH = /^[^<>:"|?*\0]+$/;
|
||||
|
||||
function getPluginStates() {
|
||||
const states = getConfig("pluginStates");
|
||||
if (states && typeof states === "object") {
|
||||
return states as Record<string, boolean>;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function setPluginState(pluginId: string, enabled: boolean) {
|
||||
const states = getPluginStates();
|
||||
states[pluginId] = enabled;
|
||||
setConfig("pluginStates", states);
|
||||
}
|
||||
|
||||
function getLogPrefix(id: string) {
|
||||
return `[Plugin:${id}]`;
|
||||
}
|
||||
|
||||
function parseManifest(pluginDir: string): PluginManifest | null {
|
||||
const manifestPath = path.join(pluginDir, "manifest.json");
|
||||
if (!existsSync(manifestPath)) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(manifestPath, "utf-8")) as PluginManifest;
|
||||
if (!parsed.id || !parsed.name || !parsed.version) return null;
|
||||
if (!VALID_PLUGIN_ID.test(parsed.id)) {
|
||||
console.error(`[Plugin Manager] Invalid plugin id "${parsed.id}"`);
|
||||
return null;
|
||||
}
|
||||
if (typeof parsed.name !== "string" || parsed.name.length > 128) {
|
||||
console.error(`[Plugin Manager] ${parsed.id}: invalid plugin name`);
|
||||
return null;
|
||||
}
|
||||
if (typeof parsed.version !== "string" || parsed.version.length > 64) {
|
||||
console.error(`[Plugin Manager] ${parsed.id}: invalid plugin version`);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
typeof parsed.main === "undefined" &&
|
||||
typeof parsed.preload === "undefined" &&
|
||||
typeof parsed.renderer === "undefined"
|
||||
) {
|
||||
console.error(`[Plugin Manager] ${parsed.id}: at least one of main/preload/renderer must be defined`);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
(parsed.main && !VALID_ENTRY_PATH.test(parsed.main)) ||
|
||||
(parsed.preload && !VALID_ENTRY_PATH.test(parsed.preload)) ||
|
||||
(parsed.renderer && !VALID_ENTRY_PATH.test(parsed.renderer))
|
||||
) {
|
||||
console.error(`[Plugin Manager] ${parsed.id}: invalid entry path`);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
typeof parsed.compatibleVersions !== "undefined" &&
|
||||
(!Array.isArray(parsed.compatibleVersions) ||
|
||||
parsed.compatibleVersions.some((version) => typeof version !== "string"))
|
||||
) {
|
||||
console.error(`[Plugin Manager] ${parsed.id}: compatibleVersions must be an array of strings`);
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error(`[Plugin Manager] Failed to parse manifest in ${pluginDir}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isCompatibleVersion(versionPattern: string, version: string): boolean {
|
||||
if (versionPattern === "*" || versionPattern === version) return true;
|
||||
if (versionPattern.endsWith(".x")) {
|
||||
const prefix = versionPattern.slice(0, -2);
|
||||
return version === prefix || version.startsWith(`${prefix}.`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCompatibility(manifest: PluginManifest): { compatible: boolean; message?: string } {
|
||||
const supported = manifest.compatibleVersions;
|
||||
if (!supported || supported.length === 0) {
|
||||
return { compatible: true };
|
||||
}
|
||||
const compatible = supported.some((pattern) => isCompatibleVersion(pattern, currentLegcordVersion));
|
||||
if (compatible) return { compatible: true };
|
||||
return {
|
||||
compatible: false,
|
||||
message: `Incompatible with Legcord ${currentLegcordVersion} (supports: ${supported.join(", ")})`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePluginEntry(record: PluginRecord, target: PluginTarget) {
|
||||
const entry = record.manifest[target];
|
||||
if (!entry) return null;
|
||||
const resolved = path.resolve(record.directory, entry);
|
||||
const relative = path.relative(record.directory, resolved);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
||||
if (!existsSync(resolved)) return null;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function loadMain(record: PluginRecord) {
|
||||
if (record.loadedMain) return;
|
||||
const entry = resolvePluginEntry(record, "main");
|
||||
if (!entry) return;
|
||||
|
||||
const loggerPrefix = getLogPrefix(record.manifest.id);
|
||||
const api: PluginMainApi = {
|
||||
id: record.manifest.id,
|
||||
manifest: record.manifest,
|
||||
logger: {
|
||||
log: (...args) => console.log(loggerPrefix, ...args),
|
||||
warn: (...args) => console.warn(loggerPrefix, ...args),
|
||||
error: (...args) => console.error(loggerPrefix, ...args),
|
||||
},
|
||||
patcher: {
|
||||
before,
|
||||
after,
|
||||
instead,
|
||||
},
|
||||
electron: {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
dialog,
|
||||
shell,
|
||||
},
|
||||
onCleanup: (cleanup) => {
|
||||
record.cleanups.push(cleanup);
|
||||
},
|
||||
};
|
||||
|
||||
const moduleUrl = `${pathToFileURL(entry).href}?v=${Date.now()}`;
|
||||
const mod = (await import(moduleUrl)) as {
|
||||
default?: (api: PluginMainApi) => void | Promise<void>;
|
||||
activate?: (api: PluginMainApi) => void | Promise<void>;
|
||||
};
|
||||
const entrypoint = mod.activate ?? mod.default;
|
||||
if (typeof entrypoint === "function") {
|
||||
await entrypoint(api);
|
||||
}
|
||||
record.loadedMain = true;
|
||||
}
|
||||
|
||||
function disableMain(record: PluginRecord) {
|
||||
if (!record.loadedMain) return;
|
||||
for (const cleanup of record.cleanups.splice(0)) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
console.error(`[Plugin Manager] Cleanup failed for ${record.manifest.id}`, error);
|
||||
}
|
||||
}
|
||||
record.loadedMain = false;
|
||||
}
|
||||
|
||||
export async function initializePluginSystem() {
|
||||
if (!existsSync(pluginFolder)) {
|
||||
mkdirSync(pluginFolder, { recursive: true });
|
||||
}
|
||||
|
||||
const discovered: PluginRecord[] = [];
|
||||
for (const child of readdirSync(pluginFolder)) {
|
||||
const full = path.join(pluginFolder, child);
|
||||
const manifest = parseManifest(full);
|
||||
if (!manifest) continue;
|
||||
const state = getPluginStates();
|
||||
const enabled = state[manifest.id] ?? false;
|
||||
const compatibility = getCompatibility(manifest);
|
||||
discovered.push({
|
||||
manifest,
|
||||
directory: full,
|
||||
enabled,
|
||||
compatible: compatibility.compatible,
|
||||
...(compatibility.message !== undefined ? { compatibilityMessage: compatibility.message } : {}),
|
||||
loadedMain: false,
|
||||
cleanups: [],
|
||||
});
|
||||
}
|
||||
|
||||
records.clear();
|
||||
for (const record of discovered) {
|
||||
records.set(record.manifest.id, record);
|
||||
}
|
||||
|
||||
for (const record of records.values()) {
|
||||
if (!record.enabled || !record.compatible) continue;
|
||||
await loadMain(record);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPluginEnabled(pluginId: string, enabled: boolean) {
|
||||
const record = records.get(pluginId);
|
||||
if (!record) return false;
|
||||
if (enabled && !record.compatible) return false;
|
||||
setPluginState(pluginId, enabled);
|
||||
record.enabled = enabled;
|
||||
if (enabled) {
|
||||
await loadMain(record);
|
||||
} else {
|
||||
disableMain(record);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function reloadPlugin(pluginId: string) {
|
||||
const record = records.get(pluginId);
|
||||
if (!record) return false;
|
||||
if (!record.compatible) return false;
|
||||
disableMain(record);
|
||||
if (record.enabled) {
|
||||
await loadMain(record);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function listPlugins() {
|
||||
return [...records.values()].map((record) => ({
|
||||
id: record.manifest.id,
|
||||
name: record.manifest.name,
|
||||
version: record.manifest.version,
|
||||
description: record.manifest.description,
|
||||
author: record.manifest.author,
|
||||
enabled: record.enabled,
|
||||
compatible: record.compatible,
|
||||
compatibilityMessage: record.compatibilityMessage,
|
||||
compatibleVersions: record.manifest.compatibleVersions ?? [],
|
||||
hasMain: Boolean(record.manifest.main),
|
||||
hasPreload: Boolean(record.manifest.preload),
|
||||
hasRenderer: Boolean(record.manifest.renderer),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getRuntimeEntries(target: Exclude<PluginTarget, "main">) {
|
||||
return [...records.values()]
|
||||
.filter((record) => record.enabled && record.compatible)
|
||||
.map((record) => {
|
||||
const entry = resolvePluginEntry(record, target);
|
||||
if (!entry) return null;
|
||||
return {
|
||||
id: record.manifest.id,
|
||||
name: record.manifest.name,
|
||||
path: entry,
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is { id: string; name: string; path: string } => entry !== null);
|
||||
}
|
||||
|
||||
export function getRuntimeScript(pluginId: string, target: Exclude<PluginTarget, "main">) {
|
||||
const record = records.get(pluginId);
|
||||
if (!record?.enabled || !record.compatible) return null;
|
||||
const entry = resolvePluginEntry(record, target);
|
||||
if (!entry) return null;
|
||||
return readFileSync(entry, "utf-8");
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
import type { Game } from "arrpc";
|
||||
import type { Keybind } from "../../@types/keybind.js";
|
||||
import type { LegcordWindow } from "../../@types/legcordWindow.d.ts";
|
||||
|
|
@ -12,6 +13,20 @@ interface IPCSources {
|
|||
name: string;
|
||||
thumbnail: HTMLCanvasElement;
|
||||
}
|
||||
interface LegcordPluginInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
enabled: boolean;
|
||||
compatible: boolean;
|
||||
compatibilityMessage?: string;
|
||||
compatibleVersions: string[];
|
||||
hasMain: boolean;
|
||||
hasPreload: boolean;
|
||||
hasRenderer: boolean;
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld("legcord", {
|
||||
window: {
|
||||
|
|
@ -36,6 +51,8 @@ contextBridge.exposeInMainWorld("legcord", {
|
|||
openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"),
|
||||
copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"),
|
||||
copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"),
|
||||
openWebRTCInternals: () => ipcRenderer.send("openWebRTCInternals"),
|
||||
openGPUInfo: () => ipcRenderer.send("openGPUInfo"),
|
||||
dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput,
|
||||
},
|
||||
touchbar: {
|
||||
|
|
@ -87,6 +104,7 @@ contextBridge.exposeInMainWorld("legcord", {
|
|||
uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id),
|
||||
edit: (id: string) => ipcRenderer.send("editTheme", id),
|
||||
getThemes: () => ipcRenderer.sendSync("getThemes") as ThemeManifest[],
|
||||
refresh: () => ipcRenderer.sendSync("refreshThemesCache") as ThemeManifest[],
|
||||
openImportPicker: () => ipcRenderer.send("openImportPicker"),
|
||||
set: (id: string, state: boolean) => ipcRenderer.send("setThemeEnabled", id, state),
|
||||
folder: (id: string) => ipcRenderer.send("openThemeFolder", id),
|
||||
|
|
@ -110,6 +128,13 @@ contextBridge.exposeInMainWorld("legcord", {
|
|||
ipcRenderer.invoke("backupSave", data) as Promise<{ ok: true } | { ok: false; error: string }>,
|
||||
restore: () => ipcRenderer.invoke("backupRestore") as Promise<string>,
|
||||
},
|
||||
plugins: {
|
||||
list: () => ipcRenderer.invoke("plugins:list") as Promise<LegcordPluginInfo[]>,
|
||||
setEnabled: (id: string, enabled: boolean) =>
|
||||
ipcRenderer.invoke("plugins:set-enabled", id, enabled) as Promise<{ ok: boolean }>,
|
||||
reload: (id: string) => ipcRenderer.invoke("plugins:reload", id) as Promise<{ ok: boolean }>,
|
||||
openFolder: () => ipcRenderer.send("openRuntimePluginsFolder"),
|
||||
},
|
||||
fs: {
|
||||
/**
|
||||
* Write a file in this plugin's scoped storage (e.g. "cache/deleted-messages.json").
|
||||
|
|
|
|||
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);
|
||||
})();
|
||||
`);
|
||||
})();
|
||||
});
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
const { ipcRenderer, webFrame } = require("electron");
|
||||
|
||||
import type { ModBundle } from "../../../@types/ModBundle.js";
|
||||
|
||||
async function inject() {
|
||||
try {
|
||||
await ipcRenderer.invoke("getCustomBundle").then(async (bundle: ModBundle) => {
|
||||
if (bundle.enabled) {
|
||||
if (bundle?.enabled) {
|
||||
await webFrame.executeJavaScript(bundle.js);
|
||||
if (bundle.css) {
|
||||
webFrame.insertCSS(bundle.css); //NOTE - Custom mods might require CSS.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
const { ipcRenderer, webFrame } = require("electron");
|
||||
|
||||
import type { ModBundle } from "../../../@types/ModBundle.js";
|
||||
|
||||
async function inject() {
|
||||
try {
|
||||
await ipcRenderer.invoke("getEquicordBundle").then(async (bundle: ModBundle) => {
|
||||
if (bundle.enabled) {
|
||||
if (bundle?.enabled) {
|
||||
await webFrame.executeJavaScript(bundle.js);
|
||||
webFrame.insertCSS(bundle.css!); //NOTE - Equicord requires CSS.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { ipcRenderer, webFrame } = require("electron");
|
||||
|
||||
import type { ModBundle } from "../../../@types/ModBundle.js";
|
||||
|
||||
const requiredPlugins: Record<string, [string, { isVisible: boolean; allowedActions: Record<string, true> }]> = {
|
||||
|
|
@ -17,7 +18,7 @@ if (process.platform === "darwin") {
|
|||
async function inject() {
|
||||
try {
|
||||
await ipcRenderer.invoke("getShelterBundle").then(async (bundle: ModBundle) => {
|
||||
if (bundle.enabled) {
|
||||
if (bundle?.enabled) {
|
||||
await webFrame.executeJavaScript(`(()=>{
|
||||
const SHELTER_INJECTOR_PLUGINS = ${JSON.stringify(requiredPlugins)};
|
||||
${bundle.js}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
const { ipcRenderer, webFrame } = require("electron");
|
||||
|
||||
import type { ModBundle } from "../../../@types/ModBundle.js";
|
||||
|
||||
async function inject() {
|
||||
try {
|
||||
await ipcRenderer.invoke("getVencordBundle").then(async (bundle: ModBundle) => {
|
||||
if (bundle.enabled) {
|
||||
if (bundle?.enabled) {
|
||||
await webFrame.executeJavaScript(bundle.js);
|
||||
webFrame.insertCSS(bundle.css!); //NOTE - Vencord requires CSS.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
const { ipcRenderer } = require("electron");
|
||||
|
||||
import { addStyle } from "../../common/dom.js";
|
||||
import { sleep } from "../../common/sleep.js";
|
||||
|
||||
if (
|
||||
ipcRenderer.sendSync("getConfig", "windowStyle") === "default" ||
|
||||
ipcRenderer.sendSync("getConfig", "windowStyle") === "overlay"
|
||||
) {
|
||||
const windowStyle = ipcRenderer.sendSync("getConfig", "windowStyle") as string;
|
||||
const transparency = ipcRenderer.sendSync("getConfig", "transparency") as string;
|
||||
const os = ipcRenderer.sendSync("getOS") as string;
|
||||
|
||||
// Native + transparency on macOS uses overlay chrome (see createWindow / Legcord#1095).
|
||||
const usesOverlayChrome =
|
||||
windowStyle === "default" ||
|
||||
windowStyle === "overlay" ||
|
||||
(windowStyle === "native" && os === "darwin" && transparency !== "none");
|
||||
|
||||
if (usesOverlayChrome) {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.body.setAttribute("legcord-platform", ipcRenderer.sendSync("getOS"));
|
||||
document.body.setAttribute("legcord-platform", os);
|
||||
addStyle("legcord://assets/css/baseTitlebar.css");
|
||||
sleep(500);
|
||||
switch (ipcRenderer.sendSync("getOS")) {
|
||||
switch (os) {
|
||||
case "darwin":
|
||||
// breaks traffic lights with bar__ and hidden__ classes
|
||||
// document.body.setAttribute("class", "platform-osx");
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
type OptimizableFunction<T extends Node> = (child: T) => T;
|
||||
|
||||
const optimize = <T extends Node>(orig: OptimizableFunction<T>) => {
|
||||
return function (this: Element, ...args: [Element]): T | number {
|
||||
if (typeof args[0]?.className === "string" && args[0].className.includes("activity")) {
|
||||
// fix by xql.dev <@1356430365774053448>
|
||||
setTimeout(() => orig.apply(this, args as unknown as [T]), 100);
|
||||
return args[0] as unknown as T;
|
||||
}
|
||||
return function (this: Element, ...args: [Element]): T {
|
||||
return orig.apply(this, args as unknown as [T]);
|
||||
} as unknown as OptimizableFunction<T>;
|
||||
};
|
||||
|
||||
// We are taking in the function itself
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
Element.prototype.removeChild = optimize(Element.prototype.removeChild);
|
||||
|
||||
// Thanks Ari - <@1249446413952225452>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { addScript, addStyle, injectJS } from "../../common/dom.js";
|
||||
import { sleep } from "../../common/sleep.js";
|
||||
|
||||
const { ipcRenderer } = require("electron");
|
||||
const version = ipcRenderer.sendSync("displayVersion") as string;
|
||||
|
||||
|
|
@ -29,15 +30,123 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
|
|||
}
|
||||
}
|
||||
|
||||
// Raise Chromium's ~2500kbps screenshare SDP cap before Discord binds RTCPeerConnection.
|
||||
// Shelter plugins load too late / MediaEngine may keep the original method reference.
|
||||
// On macOS/Windows also rewrite H.264 Constrained Baseline → Baseline on local *and* remote SDP:
|
||||
// Discord's Go Live answer forces profile-level-id=42e01f (OpenH264); local-only munging
|
||||
// is overwritten by setRemoteDescription, so the answer must be rewritten too.
|
||||
// Without this, Windows burns CPU on OpenH264 even when Media Foundation HW encode is available.
|
||||
// Gated by settings.sdpH264BaselineRewrite (default on) so users can disable if negotiation breaks.
|
||||
{
|
||||
const rewriteBaselineSetting = ipcRenderer.sendSync("getConfig", "sdpH264BaselineRewrite") as boolean | undefined;
|
||||
const preferHwH264 =
|
||||
(process.platform === "darwin" || process.platform === "win32") && (rewriteBaselineSetting ?? true);
|
||||
const bitrateScript = document.createElement("script");
|
||||
bitrateScript.textContent = `(function () {
|
||||
var CAP = "80000";
|
||||
// Modest floor/start (kbps) so GCC probes above Discord's ~1–2.5 Mbps screenshare default
|
||||
// without fighting congestion control. Max remains the hard ceiling.
|
||||
var MIN_BR = "3000";
|
||||
var START_BR = "3000";
|
||||
// VideoToolbox (macOS) and Media Foundation (Windows) HW encode avoid OpenH264 CBP.
|
||||
var preferHwH264 = ${preferHwH264 ? "true" : "false"};
|
||||
function setOrAppendFmtpParam(sdp, key, value) {
|
||||
var re = new RegExp(key + "=\\\\d+", "g");
|
||||
if (sdp.indexOf(key + "=") !== -1) {
|
||||
return sdp.replace(re, key + "=" + value);
|
||||
}
|
||||
return sdp.replace(/(a=fmtp:\\d+ [^\\r\\n]*)/g, function (line) {
|
||||
if (line.indexOf(key + "=") !== -1) return line;
|
||||
return line + ";" + key + "=" + value;
|
||||
});
|
||||
}
|
||||
function mungeSdp(sdp) {
|
||||
if (!sdp || typeof sdp !== "string") return sdp;
|
||||
var out = sdp;
|
||||
// Chromium uses OpenH264 for Constrained Baseline (42e0xx). Rewrite to Baseline
|
||||
// (4200xx) so VideoToolbox / Media Foundation HW H.264 can be selected.
|
||||
// See discuss-webrtc: CBP uses software encoder for historical reasons.
|
||||
if (preferHwH264) {
|
||||
out = out.replace(/profile-level-id=42e0([0-9a-fA-F]{2})/gi, "profile-level-id=4200$1");
|
||||
}
|
||||
out = setOrAppendFmtpParam(out, "x-google-max-bitrate", CAP);
|
||||
out = setOrAppendFmtpParam(out, "x-google-min-bitrate", MIN_BR);
|
||||
out = setOrAppendFmtpParam(out, "x-google-start-bitrate", START_BR);
|
||||
return out;
|
||||
}
|
||||
function wrapDescription(desc) {
|
||||
if (!desc || !desc.sdp) return desc;
|
||||
var sdp = mungeSdp(desc.sdp);
|
||||
if (sdp === desc.sdp) return desc;
|
||||
try {
|
||||
return new RTCSessionDescription({ type: desc.type, sdp: sdp });
|
||||
} catch (e) {
|
||||
return Object.assign({}, desc, { sdp: sdp });
|
||||
}
|
||||
}
|
||||
var proto = window.RTCPeerConnection && window.RTCPeerConnection.prototype;
|
||||
if (!proto) return;
|
||||
var origSLD = proto.setLocalDescription;
|
||||
proto.setLocalDescription = function (desc) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
if (args.length > 0) args[0] = wrapDescription(args[0]);
|
||||
return origSLD.apply(this, args);
|
||||
};
|
||||
var origSRD = proto.setRemoteDescription;
|
||||
proto.setRemoteDescription = function (desc) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
if (args.length > 0) args[0] = wrapDescription(args[0]);
|
||||
return origSRD.apply(this, args);
|
||||
};
|
||||
var origOffer = proto.createOffer;
|
||||
proto.createOffer = function () {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
return Promise.resolve(origOffer.apply(self, args)).then(function (offer) {
|
||||
return wrapDescription(offer) || offer;
|
||||
});
|
||||
};
|
||||
var origAnswer = proto.createAnswer;
|
||||
if (origAnswer) {
|
||||
proto.createAnswer = function () {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
return Promise.resolve(origAnswer.apply(self, args)).then(function (answer) {
|
||||
return wrapDescription(answer) || answer;
|
||||
});
|
||||
};
|
||||
}
|
||||
// Do NOT patch RTCRtpSender.setParameters to force high maxBitrate — that fights
|
||||
// Discord/WebRTC congestion control and collapses streams to tiny resolutions.
|
||||
console.log("[Legcord] Early WebRTC screenshare SDP patch installed" + (preferHwH264 ? " (H264 CBP→Baseline on local+remote)" : ""));
|
||||
})();`;
|
||||
|
||||
if (document.documentElement) {
|
||||
document.documentElement.prepend(bitrateScript);
|
||||
} else {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (document.documentElement) {
|
||||
observer.disconnect();
|
||||
document.documentElement.prepend(bitrateScript);
|
||||
}
|
||||
});
|
||||
observer.observe(document, { childList: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Fix: Chromium on macOS ignores video deviceId when passed as an "ideal" constraint
|
||||
// (plain string), always returning the first camera. Discord passes deviceId this way.
|
||||
// This patch promotes "ideal" to "exact", stops active tracks before switching so macOS
|
||||
// releases the hardware, and falls back to the original behavior if "exact" fails.
|
||||
// Injected into the page context because contextIsolation is enabled.
|
||||
// See: https://github.com/electron/electron/issues/44502
|
||||
// Stopping prior audio streams on every new getUserMedia breaks Discord on Windows/Linux
|
||||
// (multiple object-shaped audio requests); keep that behavior only on darwin.
|
||||
{
|
||||
const stopPrevAudioStreams = process.platform === "darwin";
|
||||
const cameraFixScript = document.createElement("script");
|
||||
cameraFixScript.textContent = `(function() {
|
||||
var legcordStopPrevAudioStreams = ${stopPrevAudioStreams};
|
||||
var _origGUM = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
||||
var _activeVideoStreams = [];
|
||||
var _activeAudioStreams = [];
|
||||
|
|
@ -58,16 +167,16 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
|
|||
function trackStream(stream) {
|
||||
var ref = new WeakRef(stream);
|
||||
if (stream.getVideoTracks().length > 0) _activeVideoStreams.push(ref);
|
||||
if (stream.getAudioTracks().length > 0) _activeAudioStreams.push(ref);
|
||||
if (legcordStopPrevAudioStreams && stream.getAudioTracks().length > 0) _activeAudioStreams.push(ref);
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia = async function(constraints) {
|
||||
var hasVideo = constraints && constraints.video && typeof constraints.video !== "boolean";
|
||||
var hasAudio = constraints && constraints.audio && typeof constraints.audio !== "boolean";
|
||||
|
||||
// Release previous hardware when new request comes in for the same kind
|
||||
// Release previous hardware when new request comes in for the same kind (audio: darwin only)
|
||||
if (hasVideo && _activeVideoStreams.length > 0) stopTrackedStreams(_activeVideoStreams, "video");
|
||||
if (hasAudio && _activeAudioStreams.length > 0) stopTrackedStreams(_activeAudioStreams, "audio");
|
||||
if (legcordStopPrevAudioStreams && hasAudio && _activeAudioStreams.length > 0) stopTrackedStreams(_activeAudioStreams, "audio");
|
||||
|
||||
var hasStringVideoDeviceId = hasVideo && typeof constraints.video.deviceId === "string";
|
||||
if (!hasStringVideoDeviceId) {
|
||||
|
|
@ -127,40 +236,9 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
|
|||
}
|
||||
}
|
||||
|
||||
export async function getVirtmic() {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
|
||||
return audioDevice?.deviceId;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
await sleep(5000).then(() => {
|
||||
const original = navigator.mediaDevices.getDisplayMedia;
|
||||
navigator.mediaDevices.getDisplayMedia = async function (opts) {
|
||||
const stream = await original.call(this, opts);
|
||||
const id = await getVirtmic();
|
||||
|
||||
if (id) {
|
||||
const audio = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
deviceId: {
|
||||
exact: id,
|
||||
},
|
||||
autoGainControl: false,
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
},
|
||||
});
|
||||
audio.getAudioTracks().forEach((t) => stream.addTrack(t));
|
||||
}
|
||||
|
||||
return stream;
|
||||
};
|
||||
|
||||
// Venmic audio injection lives in the Shelter screenshare getDisplayMedia patch.
|
||||
// dirty hack to make clicking notifications focus Legcord
|
||||
addScript(`
|
||||
(() => {
|
||||
|
|
@ -198,6 +276,7 @@ async function load() {
|
|||
el.id = "ac-ver";
|
||||
el.textContent = `Legcord Version: ${version}`;
|
||||
info.after(el);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
|
|
|||
185
src/discord/preload/plugins.mts
Normal file
185
src/discord/preload/plugins.mts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
const { ipcRenderer, webFrame } = require("electron");
|
||||
const { after, before, instead } = require("spitroast/dist/index.js");
|
||||
|
||||
type RuntimeEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
const cleanupMap = new Map<string, Array<() => void>>();
|
||||
|
||||
function addCleanup(pluginId: string, cleanup: () => void) {
|
||||
const current = cleanupMap.get(pluginId) ?? [];
|
||||
current.push(cleanup);
|
||||
cleanupMap.set(pluginId, current);
|
||||
}
|
||||
|
||||
function clearCleanup(pluginId: string) {
|
||||
const cleanups = cleanupMap.get(pluginId);
|
||||
if (!cleanups) return;
|
||||
for (const cleanup of cleanups.splice(0)) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
console.error(`[Plugin:${pluginId}] cleanup failed`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createApi(pluginId: string, pluginName: string) {
|
||||
const loggerPrefix = `[Plugin:${pluginId}]`;
|
||||
return {
|
||||
id: pluginId,
|
||||
name: pluginName,
|
||||
logger: {
|
||||
log: (...args: unknown[]) => console.log(loggerPrefix, ...args),
|
||||
warn: (...args: unknown[]) => console.warn(loggerPrefix, ...args),
|
||||
error: (...args: unknown[]) => console.error(loggerPrefix, ...args),
|
||||
},
|
||||
patcher: {
|
||||
before: (...args: Parameters<typeof before>) => {
|
||||
const unpatch = before(...args);
|
||||
addCleanup(pluginId, unpatch);
|
||||
return unpatch;
|
||||
},
|
||||
after: (...args: Parameters<typeof after>) => {
|
||||
const unpatch = after(...args);
|
||||
addCleanup(pluginId, unpatch);
|
||||
return unpatch;
|
||||
},
|
||||
instead: (...args: Parameters<typeof instead>) => {
|
||||
const unpatch = instead(...args);
|
||||
addCleanup(pluginId, unpatch);
|
||||
return unpatch;
|
||||
},
|
||||
},
|
||||
onCleanup: (cleanup: () => void) => addCleanup(pluginId, cleanup),
|
||||
};
|
||||
}
|
||||
|
||||
async function executePreloadPluginSource(_pluginId: string, source: string, api: ReturnType<typeof createApi>) {
|
||||
const runner = new Function(
|
||||
"api",
|
||||
`
|
||||
const mod = { exports: {} };
|
||||
const module = mod;
|
||||
const exports = mod.exports;
|
||||
${source}
|
||||
const activate = mod.exports.activate ?? mod.exports.default ?? globalThis.activatePlugin;
|
||||
if (typeof activate === "function") {
|
||||
return activate(api);
|
||||
}
|
||||
`,
|
||||
) as (api: ReturnType<typeof createApi>) => unknown;
|
||||
await Promise.resolve(runner(api));
|
||||
}
|
||||
|
||||
async function loadPreloadPlugins() {
|
||||
const entries = (await ipcRenderer.invoke("plugins:get-runtime-entries", "preload")) as RuntimeEntry[];
|
||||
for (const entry of entries) {
|
||||
clearCleanup(entry.id);
|
||||
try {
|
||||
const source = (await ipcRenderer.invoke("plugins:get-runtime-script", entry.id, "preload")) as
|
||||
| string
|
||||
| null;
|
||||
if (!source) continue;
|
||||
await executePreloadPluginSource(entry.id, source, createApi(entry.id, entry.name));
|
||||
} catch (error) {
|
||||
console.error(`[Plugin:${entry.id}] preload entry failed`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRendererBootstrap(pluginId: string, pluginName: string, source: string) {
|
||||
return `
|
||||
(() => {
|
||||
const g = globalThis;
|
||||
const stores = g.__legcordPluginPatches ?? (g.__legcordPluginPatches = new WeakMap());
|
||||
const unpatchAll = () => { g.__legcordPluginPatches = new WeakMap(); };
|
||||
const patch = (type, name, parent, callback, oneTime = false) => {
|
||||
if (!parent || typeof parent[name] !== "function") throw new Error(\`Cannot patch \${String(name)}\`);
|
||||
const original = parent[name];
|
||||
let bucket = stores.get(original);
|
||||
if (!bucket) {
|
||||
bucket = { o: original, b: new Map(), i: new Map(), a: new Map(), c: [] };
|
||||
const proxy = new Proxy(original, {
|
||||
apply(_target, thisArg, argArray) {
|
||||
let args = [...argArray];
|
||||
for (const hook of bucket.b.values()) {
|
||||
const next = hook.call(thisArg, args);
|
||||
if (Array.isArray(next)) args = next;
|
||||
}
|
||||
const callOriginal = (...inner) => Reflect.apply(original, thisArg, inner);
|
||||
let ret = [...bucket.i.values()].reduceRight((prev, cur) => (...inner) => cur.call(thisArg, inner, prev), callOriginal)(...args);
|
||||
for (const hook of bucket.a.values()) ret = hook.call(thisArg, args, ret) ?? ret;
|
||||
for (const cleanup of bucket.c) cleanup();
|
||||
bucket.c.length = 0;
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
stores.set(proxy, bucket);
|
||||
parent[name] = proxy;
|
||||
bucket.proxy = proxy;
|
||||
bucket.name = name;
|
||||
bucket.parent = parent;
|
||||
}
|
||||
const hookId = Symbol("hook");
|
||||
const remove = () => {
|
||||
const map = type === "b" ? bucket.b : type === "i" ? bucket.i : bucket.a;
|
||||
if (!map.delete(hookId)) return false;
|
||||
if (bucket.b.size || bucket.i.size || bucket.a.size) return true;
|
||||
bucket.parent[bucket.name] = bucket.o;
|
||||
stores.delete(bucket.proxy);
|
||||
return true;
|
||||
};
|
||||
if (oneTime) bucket.c.push(remove);
|
||||
(type === "b" ? bucket.b : type === "i" ? bucket.i : bucket.a).set(hookId, callback);
|
||||
return remove;
|
||||
};
|
||||
const api = {
|
||||
id: "${pluginId}",
|
||||
name: "${pluginName}",
|
||||
logger: {
|
||||
log: (...args) => console.log("[Plugin:${pluginId}]", ...args),
|
||||
warn: (...args) => console.warn("[Plugin:${pluginId}]", ...args),
|
||||
error: (...args) => console.error("[Plugin:${pluginId}]", ...args),
|
||||
},
|
||||
patcher: {
|
||||
before: (name, parent, cb, once) => patch("b", name, parent, cb, once),
|
||||
instead: (name, parent, cb, once) => patch("i", name, parent, cb, once),
|
||||
after: (name, parent, cb, once) => patch("a", name, parent, cb, once),
|
||||
unpatchAll
|
||||
}
|
||||
};
|
||||
const mod = { exports: {} };
|
||||
const module = mod;
|
||||
const exports = mod.exports;
|
||||
${source}
|
||||
const activate = mod.exports.activate ?? mod.exports.default ?? g.activatePlugin;
|
||||
if (typeof activate === "function") activate(api);
|
||||
})();
|
||||
//# sourceURL=legcord-plugin-renderer-${pluginId}.js
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadRendererPlugins() {
|
||||
const entries = (await ipcRenderer.invoke("plugins:get-runtime-entries", "renderer")) as RuntimeEntry[];
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
const source = (await ipcRenderer.invoke("plugins:get-runtime-script", entry.id, "renderer")) as
|
||||
| string
|
||||
| null;
|
||||
if (!source) continue;
|
||||
const bootstrap = getRendererBootstrap(entry.id, entry.name, source);
|
||||
await webFrame.executeJavaScript(bootstrap);
|
||||
} catch (error) {
|
||||
console.error(`[Plugin:${entry.id}] renderer entry failed`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await loadPreloadPlugins();
|
||||
await loadRendererPlugins();
|
||||
})();
|
||||
|
|
@ -4,10 +4,12 @@ import "./mods/shelter.mjs";
|
|||
import "./mods/vencord.mjs";
|
||||
import "./mods/equicord.mjs";
|
||||
import "./mods/custom.mjs";
|
||||
import "./plugins.mjs";
|
||||
import "./patches.mjs";
|
||||
import "./newTitlebar.mjs";
|
||||
import "./titlebar.mjs";
|
||||
import "./themes.js";
|
||||
import "./inviteBackButton.mjs";
|
||||
|
||||
console.log("Legcord");
|
||||
window.localStorage.setItem("hideNag", "true");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { addTheme } from "../../common/dom.js";
|
||||
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
ipcRenderer.on("addTheme", (_event: unknown, name: string, css: string) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const { ipcRenderer } = require("electron");
|
||||
|
||||
import type { Settings } from "../../@types/settings.js";
|
||||
import { addStyle } from "../../common/dom.js";
|
||||
|
||||
const titlebarHTML = `<nav class="titlebar">
|
||||
<div class="window-title" id="window-title"></div>
|
||||
<div id="window-controls-container">
|
||||
|
|
|
|||
|
|
@ -1,25 +1,49 @@
|
|||
import path from "node:path";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import type { GameList } from "arrpc";
|
||||
import type { GameList, ServerSettings } from "arrpc";
|
||||
import type { BrowserWindow } from "electron";
|
||||
import { getConfig } from "../common/config.js";
|
||||
import { getDetectables } from "../common/detectables.js";
|
||||
import { navigateTo } from "../common/dom.js";
|
||||
|
||||
/** Floor so a bad/zero config value cannot busy-loop the scanner. */
|
||||
const MIN_SCAN_INTERVAL_MS = 1000;
|
||||
const DEFAULT_SCAN_INTERVAL_MS = 5000;
|
||||
|
||||
let rpcWorker: Worker;
|
||||
export let processList: GameList[] = [];
|
||||
|
||||
export function getRpcServerSettings(): ServerSettings {
|
||||
const rawInterval = Number(getConfig("scanInterval"));
|
||||
const scanInterval =
|
||||
Number.isFinite(rawInterval) && rawInterval >= MIN_SCAN_INTERVAL_MS
|
||||
? Math.floor(rawInterval)
|
||||
: DEFAULT_SCAN_INTERVAL_MS;
|
||||
|
||||
return {
|
||||
processScanning: Boolean(getConfig("processScanning")),
|
||||
windowsLegacyScanning: Boolean(getConfig("windowsLegacyScanning")),
|
||||
scanInterval,
|
||||
};
|
||||
}
|
||||
|
||||
export function startRPC(window: BrowserWindow) {
|
||||
if (rpcWorker) {
|
||||
rpcWorker.terminate();
|
||||
}
|
||||
const rpcPath = path.join(__dirname, "rpc.js");
|
||||
const settings = getRpcServerSettings();
|
||||
|
||||
rpcWorker = new Worker(rpcPath, {
|
||||
env: {
|
||||
...process.env,
|
||||
detectables: JSON.stringify(getDetectables()),
|
||||
settings: JSON.stringify(settings),
|
||||
},
|
||||
});
|
||||
|
||||
rpcWorker.on("online", () => {
|
||||
console.log("[arRPC] process started");
|
||||
console.log("[arRPC] process started", settings);
|
||||
console.log(rpcWorker.threadId);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type Streams, desktopCapturer, ipcMain, session } from "electron";
|
||||
import { desktopCapturer, ipcMain, type Streams, session } from "electron";
|
||||
import { getConfig } from "../common/config.js";
|
||||
import { mainWindows } from "./window.js";
|
||||
|
||||
|
|
@ -17,12 +17,13 @@ export function registerCustomHandler(): void {
|
|||
console.log("WebRTC Capturer detected, using native window picker.");
|
||||
if (sources[0] === undefined) return callback({});
|
||||
}
|
||||
ipcMain.removeAllListeners("startScreenshare");
|
||||
ipcMain.once("startScreenshare", (_event, id: string, name: string, audio: boolean) => {
|
||||
console.log(`ID: ${id}`);
|
||||
if (id === "none") {
|
||||
try {
|
||||
callback({});
|
||||
} catch (e) {}
|
||||
} catch (_e) {}
|
||||
} else {
|
||||
console.log(`Audio status: ${audio}`);
|
||||
const result = { id, name };
|
||||
|
|
@ -31,6 +32,7 @@ export function registerCustomHandler(): void {
|
|||
switch (process.platform) {
|
||||
case "win32":
|
||||
case "linux":
|
||||
case "darwin":
|
||||
options = { video: result };
|
||||
if (audio)
|
||||
options = {
|
||||
|
|
@ -46,6 +48,7 @@ export function registerCustomHandler(): void {
|
|||
});
|
||||
mainWindows.every((window) => {
|
||||
window.webContents.send("getSources", sources);
|
||||
return true;
|
||||
});
|
||||
},
|
||||
{ useSystemPicker: getConfig("useMacSystemPicker") },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { TouchBar, app, nativeImage } from "electron";
|
||||
import { app, nativeImage, TouchBar } from "electron";
|
||||
import { navigateTo } from "../common/dom.js";
|
||||
import { deafenToggle, leaveCall, muteToggle } from "../common/keybindActions.js";
|
||||
import { getLang } from "../common/lang.js";
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue