Compare commits

..

No commits in common. "c8d91f61296019bb0c45f375535de8c93cf26ee1" and "97942ebf9c83469dbd3468a5c3aee881636be825" have entirely different histories.

165 changed files with 4477 additions and 10754 deletions

View file

@ -1,35 +0,0 @@
## 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 Normal file
View file

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

View file

@ -1,28 +0,0 @@
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 }}

View file

@ -7,8 +7,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Biome - name: Setup Biome
uses: biomejs/setup-biome@v2 uses: biomejs/setup-biome@v2
with:
version: 1.9.4
- name: Run Biome - name: Run Biome
run: biome ci . --reporter=github run: biome ci . --reporter=github

View file

@ -13,14 +13,14 @@ jobs:
update: update:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v6 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Use Node.js 22 - name: Use Node.js 22
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version-file: package.json node-version-file: package.json
cache: pnpm cache: pnpm

View file

@ -1,121 +1,122 @@
name: Package name: Package
on: on:
push: push:
branches: branches:
- dev - dev
- stable - stable
jobs: jobs:
package: package:
continue-on-error: true continue-on-error: true
strategy: strategy:
matrix: matrix:
os: [macos-latest, windows-latest, ubuntu-latest] os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Add commit to version
if: github.ref_name == 'dev'
run: cat <<< $(jq --arg ver "$(jq -r '.version' package.json)-$(git rev-parse --short HEAD)" '.version = $ver' package.json) > package.json
shell: bash
- name: Prepare PNPM
uses: pnpm/action-setup@v6
- name: Prepare Node.js
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: pnpm
- name: Install dependencies
run: pnpm i
- name: Build TypeScript
run: pnpm build
- name: Install SnapCraft
if: matrix.os == 'macos-latest'
uses: samuelmeuli/action-snapcraft@v3
- name: Load Electron cache
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)
if: matrix.os == 'macos-latest'
run: pnpm electron-builder --universal -m zip dmg
env: env:
CSC_LINK: "https://legcord.app/NewSign.p12" SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
CSC_KEY_PASSWORD: ${{ secrets.MACOS_SIGN_PASS }} GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
APPLE_ID: ${{secrets.APPLE_ID}}
APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_ID_PASSWORD}}
APPLE_TEAM_ID: ${{secrets.APPLE_TEAM_ID}}
- name: Build Electron for Windows (NSIS, AppX, & ZIP) steps:
if: matrix.os == 'windows-latest' - name: Checkout code
run: pnpm electron-builder --ia32 --arm64 --x64 -w nsis appx zip uses: actions/checkout@v4
- name: Build Electron for Linux (RPM, DEB, AppImage & ZIP) - name: Add commit to version
if: matrix.os == 'ubuntu-latest' if: github.ref_name == 'dev'
run: pnpm electron-builder --armv7l --arm64 --x64 -l rpm deb appimage zip tar.gz run: cat <<< $(jq --arg ver "$(jq -r '.version' package.json)-$(git rev-parse --short HEAD)" '.version = $ver' package.json) > package.json
shell: bash
- name: Save Electron Cache - name: Prepare PNPM
uses: actions/cache/save@v5 uses: pnpm/action-setup@v4
with:
path: .cache - name: Prepare Node.js
key: electron-zips.${{matrix.os}} uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: pnpm
- name: Install dependencies
run: pnpm i
- name: Build TypeScript
run: pnpm build
- name: Collect artifacts - name: Install SnapCraft
run: mkdir artifacts | if: matrix.os == 'macos-latest'
find dist/. -maxdepth 1 -type f -exec mv {} artifacts \; uses: samuelmeuli/action-snapcraft@v3
shell: bash
- name: Upload artifactsstable - name: Load Electron cache
uses: actions/upload-artifact@v6 uses: actions/cache/restore@v4
with: with:
name: ${{matrix.os}}-artifacts path: .cache
path: artifacts/* key: electron-zips.${{matrix.os}}
release: # Sadly, it makes more sense to separate builds per platform
runs-on: ubuntu-latest - name: Build Electron for MacOS (DMG & ZIP)
needs: if: matrix.os == 'macos-latest'
- package run: pnpm electron-builder --universal -m zip dmg
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} CSC_LINK: "https://legcord.app/NewSign.p12"
steps: CSC_KEY_PASSWORD: ${{ secrets.MACOS_SIGN_PASS }}
- name: Download artifacts APPLE_ID: ${{secrets.APPLE_ID}}
uses: actions/download-artifact@v6 APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_ID_PASSWORD}}
with: APPLE_TEAM_ID: ${{secrets.APPLE_TEAM_ID}}
path: release-files
- name: Create release - name: Build Electron for Windows (NSIS, AppX, & ZIP)
if: github.ref_name == 'dev' if: matrix.os == 'windows-latest'
uses: ncipollo/release-action@v1 run: pnpm electron-builder --ia32 --arm64 --x64 -w nsis appx zip
with:
name: Rolling Dev Build
allowUpdates: true
removeArtifacts: true
prerelease: true
body: "Built against https://github.com/Legcord/Legcord/tree/dev on every commit. NOTE: tarballs do not update."
draft: false
tag: devbuild
artifacts: release-files/**/*
- name: Create release - name: Build Electron for Linux (RPM, DEB, AppImage & ZIP)
if: github.ref_name == 'stable' if: matrix.os == 'ubuntu-latest'
uses: ncipollo/release-action@v1 run: pnpm electron-builder --armv7l --arm64 --x64 -l rpm deb appimage zip tar.gz
with:
name: Stable Release Draft - name: Save Electron Cache
prerelease: false uses: actions/cache/save@v4
tag: stable with:
draft: true path: .cache
artifacts: release-files/**/* key: electron-zips.${{matrix.os}}
- name: Collect artifacts
run:
mkdir artifacts |
find dist/. -maxdepth 1 -type f -exec mv {} artifacts \;
shell: bash
- name: Upload artifactsstable
uses: actions/upload-artifact@v4
with:
name: ${{matrix.os}}-artifacts
path: artifacts/*
release:
runs-on: ubuntu-latest
needs:
- package
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: release-files
- name: Create release
if: github.ref_name == 'dev'
uses: ncipollo/release-action@v1
with:
name: Rolling Dev Build
allowUpdates: true
removeArtifacts: true
prerelease: true
body: "Built against https://github.com/Legcord/Legcord/tree/dev on every commit. NOTE: tarballs do not update."
draft: false
tag: devbuild
artifacts: release-files/**/*
- name: Create release
if: github.ref_name == 'stable'
uses: ncipollo/release-action@v1
with:
name: Stable Release Draft
prerelease: false
tag: stable
draft: true
artifacts: release-files/**/*

2
.gitignore vendored
View file

@ -6,4 +6,4 @@ package-lock.json
.pnpm-store .pnpm-store
.cache .cache
.DS_Store .DS_Store
scripts/spdx-license.txt pnpm-workspace.yaml

18
.vscode/launch.json vendored
View file

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

View file

@ -1,114 +0,0 @@
# 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.

View file

@ -141,7 +141,7 @@ Legcord is also available in [Pi-Apps](https://github.com/Botspot/pi-apps).
### Compiling ### Compiling
Alternatively, you can run Legcord from source ([NodeJS v26 and newer](https://nodejs.dev) and [pnpm](https://pnpm.io/installation#using-npm)) are required: Alternatively, you can run Legcord from source ([NodeJS](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` 1. Clone Legcord repo: `git clone https://github.com/Legcord/Legcord.git`
2. Run `pnpm install` to install dependencies 2. Run `pnpm install` to install dependencies

View file

@ -11,7 +11,7 @@
clear: both; clear: both;
height: var(--custom-app-top-bar-height); height: var(--custom-app-top-bar-height);
line-height: 30px; line-height: 30px;
-webkit-app-region: drag; -webkit-app-region: drag !important;
user-select: none; user-select: none;
background-color: var(--background-base-lowest); background-color: var(--background-base-lowest);
-webkit-user-select: none; -webkit-user-select: none;

View file

@ -1,9 +1,9 @@
[legcord-platform="darwin"] .sidebar-1tnWFu { [legcord-platform="darwin"] .sidebar-1tnWFu {
border-top-left-radius: 0px; border-top-left-radius: 0px !important;
} }
.platform-osx .wrapper_ef3116 { .platform-osx .wrapper_ef3116 {
margin-top: 0px; margin-top: 0px !important;
} }
[legcord-platform="darwin"] #window-controls-container { [legcord-platform="darwin"] #window-controls-container {
@ -16,6 +16,45 @@
transform: translate(-82px, 0px); 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 { [legcord-platform="darwin"] #window-controls-container #minimize {
background-color: #fac536; background-color: #fac536;
transition: background-color 0.1s ease-in; transition: background-color 0.1s ease-in;
@ -77,45 +116,6 @@
} }
div[class*="bar__"][class*="hidden__"] { div[class*="bar__"][class*="hidden__"] {
pointer-events: unset; pointer-events: unset !important;
visibility: unset; visibility: unset !important;
}
[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);
} }

View file

@ -11,13 +11,13 @@
} }
div:has(> span [data-list-item-id="guildsnav___app-download-button"]) { div:has(> span [data-list-item-id="guildsnav___app-download-button"]) {
display: none; display: none !important;
} }
/* custom keybinds are not supported on web information */ /* custom keybinds are not supported on web information */
.container__6436f.info__6436f.browserNotice__740f2 { .container__6436f.info__6436f.browserNotice__740f2 {
visibility: hidden; visibility: hidden;
display: block; display: block !important;
} }
.container__6436f.info__6436f.browserNotice__740f2:after { .container__6436f.info__6436f.browserNotice__740f2:after {
content: "You can modify global keybinds using the keybind maker on the left sidebar"; content: "You can modify global keybinds using the keybind maker on the left sidebar";

View file

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

View file

@ -13,50 +13,6 @@
background-color: var(--interactive-background-hover); background-color: var(--interactive-background-hover);
transition: 0.2s ease; transition: 0.2s ease;
} }
[legcord-platform="linux"] #window-controls-container #minimize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="linux"] #window-controls-container #maximize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="linux"] #window-controls-container #quit {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="linux"] #window-controls-container #quit-icon {
background-color: var(--interactive-icon-default);
display: list-item;
-webkit-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%;
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;
-webkit-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 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
no-repeat 50% 50%;
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 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
no-repeat 50% 50%;
}
[legcord-platform="linux"] #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='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%;
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 { [legcord-platform="linux"] #window-controls-container #minimize:hover #minimize-icon {
background-color: var(--interactive-text-hover); background-color: var(--interactive-text-hover);
transition: 0.2s ease; transition: 0.2s ease;
@ -73,6 +29,50 @@
background-color: #ffffff; background-color: #ffffff;
transition: 0.1s ease; transition: 0.1s ease;
} }
[legcord-platform="linux"] #window-controls-container #minimize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="linux"] #window-controls-container #maximize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="linux"] #window-controls-container #quit {
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;
-webkit-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%;
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 #minimize-icon {
background-color: var(--interactive-icon-default);
display: list-item;
-webkit-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 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
no-repeat 50% 50%;
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 4.399V5.5H0V4.399h11z' fill='%23000'/></svg>")
no-repeat 50% 50%;
}
[legcord-platform="linux"] #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='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%;
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"][isMaximized] #window-controls-container #maximize-icon { [legcord-platform="linux"][isMaximized] #window-controls-container #maximize-icon {
background-color: var(--interactive-icon-default); background-color: var(--interactive-icon-default);

View file

@ -11,7 +11,7 @@
clear: both; clear: both;
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
-webkit-app-region: drag; -webkit-app-region: drag !important;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
position: fixed; position: fixed;
@ -58,22 +58,6 @@
line-height: 45px; line-height: 45px;
transform: translateY(-8px); 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 { [legcord-platform="linux"] .titlebar #window-controls-container #minimize:hover {
background-color: var(--interactive-background-hover); background-color: var(--interactive-background-hover);
transition: 0.2s ease; transition: 0.2s ease;
@ -103,6 +87,22 @@
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>") 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%; 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 { [legcord-platform="linux"][isMaximized] .titlebar #window-controls-container #maximize-icon {
background-color: var(--interactive-icon-default); background-color: var(--interactive-icon-default);
@ -145,51 +145,6 @@
[legcord-platform="win32"] .titlebar #window-controls-container { [legcord-platform="win32"] .titlebar #window-controls-container {
width: 142px; width: 142px;
} }
[legcord-platform="win32"] .titlebar #window-controls-container #minimize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #maximize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit:active {
background-color: #f1707a;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit:active #quit-icon {
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;
-webkit-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%;
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"] .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='11' height='11' 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='11' height='11' 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="win32"] .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='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%;
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 { [legcord-platform="win32"] .titlebar #window-controls-container #minimize:hover {
background-color: var(--interactive-background-hover); background-color: var(--interactive-background-hover);
transition: 0.2s ease; transition: 0.2s ease;
@ -214,6 +169,50 @@
background-color: #ffffff; background-color: #ffffff;
transition: 0.1s ease; transition: 0.1s ease;
} }
[legcord-platform="win32"] .titlebar #window-controls-container #minimize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #maximize {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit {
background-color: transparent;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit:active {
background-color: #f1707a;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit:active #quit-icon {
background-color: #000000cc;
transition: 0.1s ease;
}
[legcord-platform="win32"] .titlebar #window-controls-container #quit-icon {
background-color: var(--interactive-icon-default);
display: list-item;
-webkit-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%;
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"] .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='11' height='11' 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='11' height='11' 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="win32"] .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='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%;
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"][isMaximized] .titlebar #window-controls-container #maximize-icon { [legcord-platform="win32"][isMaximized] .titlebar #window-controls-container #maximize-icon {
background-color: var(--interactive-icon-default); background-color: var(--interactive-icon-default);
@ -227,11 +226,11 @@
/* Legcord on macOS */ /* Legcord on macOS */
[legcord-platform="darwin"] .sidebar-1tnWFu { [legcord-platform="darwin"] .sidebar-1tnWFu {
border-top-left-radius: 0px; border-top-left-radius: 0px !important;
} }
[legcord-platform="darwin"] .container__037ed { [legcord-platform="darwin"] .container__037ed {
overflow: unset; overflow: unset !important;
padding-top: 48px; padding-top: 48px;
top: -48px; top: -48px;
} }
@ -257,7 +256,7 @@
[legcord-platform="darwin"][unFocused] .titlebar #window-controls-container #minimize, [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 #maximize,
[legcord-platform="darwin"][unFocused] .titlebar #window-controls-container #quit { [legcord-platform="darwin"][unFocused] .titlebar #window-controls-container #quit {
background-color: #d6d6d5; background-color: #d6d6d5 !important;
pointer-events: none; pointer-events: none;
transition: background-color 0.1s ease-in; transition: background-color 0.1s ease-in;
} }
@ -339,11 +338,7 @@
[legcord-platform="darwin"] .titlebar #window-controls-container #minimize-icon, [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 #maximize-icon,
/* biome-ignore lint/style/noDescendingSpecificity: cross-platform */ [legcord-platform="darwin"] .titlebar #window-controls-container #quit-icon {
[legcord-platform="darwin"]
.titlebar
#window-controls-container
#quit-icon {
display: none; display: none;
} }
@ -351,6 +346,12 @@
display: none; display: none;
} }
[legcord-platform="darwin"] .window-title {
float: none;
position: fixed;
left: 50%;
}
.window-title { .window-title {
content: url("legcord://assets/Wordmark.png"); content: url("legcord://assets/Wordmark.png");
height: 15px; height: 15px;
@ -361,12 +362,6 @@
filter: invert(30%); filter: invert(30%);
} }
[legcord-platform="darwin"] .window-title {
float: none;
position: fixed;
left: 50%;
}
div#app-mount { div#app-mount {
height: calc(100% - 30px); height: calc(100% - 30px);
position: absolute; position: absolute;

View file

@ -15,6 +15,22 @@
background-color: var(--interactive-background-hover); background-color: var(--interactive-background-hover);
transition: 0.2s ease; 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 { [legcord-platform="win32"] #window-controls-container #minimize {
background-color: transparent; background-color: transparent;
transition: 0.1s ease; transition: 0.1s ease;
@ -27,14 +43,6 @@
background-color: transparent; background-color: transparent;
transition: 0.1s ease; transition: 0.1s ease;
} }
[legcord-platform="win32"] #window-controls-container #quit-icon {
background-color: var(--interactive-icon-default);
display: list-item;
-webkit-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%;
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 { [legcord-platform="win32"] #window-controls-container #quit:active {
background-color: #f1707a; background-color: #f1707a;
transition: 0.1s ease; transition: 0.1s ease;
@ -43,6 +51,14 @@
background-color: #000000cc; background-color: #000000cc;
transition: 0.1s ease; transition: 0.1s ease;
} }
[legcord-platform="win32"] #window-controls-container #quit-icon {
background-color: var(--interactive-icon-default);
display: list-item;
-webkit-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%;
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 #minimize-icon { [legcord-platform="win32"] #window-controls-container #minimize-icon {
background-color: var(--interactive-icon-default); background-color: var(--interactive-icon-default);
display: list-item; display: list-item;
@ -59,22 +75,6 @@
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>") 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%; 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 { [legcord-platform="win32"][isMaximized] #window-controls-container #maximize-icon {
background-color: var(--interactive-icon-default); background-color: var(--interactive-icon-default);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View file

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

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -27,8 +27,8 @@
"settings-transparency-universal": "Universell", "settings-transparency-universal": "Universell",
"settings-transparency-modern": "Modern", "settings-transparency-modern": "Modern",
"settings-theme-transparent": "Transparent", "settings-theme-transparent": "Transparent",
"settings-transparency-tahoe-warning": "Transparenz kann zu großem Lags auf macOS 26 Tahoe führen.", "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": "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-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": "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.", "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-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": "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-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": "Startfenster", "settings-startMinimized": "Minimiert starten",
"settings-startMinimized-desc": "Legt fest, wie Legcord beim Start erscheint. Tray-Modus blendet das Fenster vollständig aus (Tray-Symbol muss aktiviert sein).", "settings-startMinimized-desc": "Legcord startet im Hintergrund und bleibt Ihnen nicht im Weg.",
"settings-useSystemCssEditor": "System-CSS-Editor verwenden", "settings-useSystemCssEditor": "System-CSS-Editor verwenden",
"settings-useSystemCssEditor-desc": "Verwenden Sie den System-CSS-Editor, um CSS zu bearbeiten.", "settings-useSystemCssEditor-desc": "Verwenden Sie den System-CSS-Editor, um CSS zu bearbeiten.",
"settings-MultiInstance": "Mehrfachinstanz", "settings-MultiInstance": "Mehrfachinstanz",
@ -89,12 +89,7 @@
"settings-prfmMode": "Leistungsmodus", "settings-prfmMode": "Leistungsmodus",
"settings-prfmMode-desc": "Der Leistungsmodus ist eine experimentelle Funktion in Legcord, die darauf ausgelegt ist, die Reaktionsfähigkeit und Leistung entsprechend Ihren Anforderungen zu optimieren. Die Auswirkungen können je nach Ihrer Hardware und Nutzung variieren. Wir empfehlen Ihnen daher, jeden Modus auszuprobieren, um herauszufinden, welcher für Sie am besten geeignet ist.", "settings-prfmMode-desc": "Der Leistungsmodus ist eine experimentelle Funktion in Legcord, die darauf ausgelegt ist, die Reaktionsfähigkeit und Leistung entsprechend Ihren Anforderungen zu optimieren. Die Auswirkungen können je nach Ihrer Hardware und Nutzung variieren. Wir empfehlen Ihnen daher, jeden Modus auszuprobieren, um herauszufinden, welcher für Sie am besten geeignet ist.",
"settings-prfmMode-performance": "Leistung", "settings-prfmMode-performance": "Leistung",
"settings-prfmMode-balanced": "Ausgewogen",
"settings-prfmMode-battery": "Batterie", "settings-prfmMode-battery": "Batterie",
"settings-prfmMode-memory": "Speichersparer",
"settings-prfmMode-voip": "Sprache & Video",
"settings-prfmMode-latency": "Niedrige Latenz",
"settings-prfmMode-smoothScreenshare": "Flüssige Bildschirmfreigabe",
"settings-prfmMode-dynamic": "Dynamisch", "settings-prfmMode-dynamic": "Dynamisch",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Automatische Verstärkung deaktivieren", "settings-disableAutogain": "Automatische Verstärkung deaktivieren",
@ -234,17 +229,5 @@
"games-removeConfirmHeader": "Spiel entfernen?", "games-removeConfirmHeader": "Spiel entfernen?",
"games-noExecutables": "Keine ausführbaren Dateien", "games-noExecutables": "Keine ausführbaren Dateien",
"games-lastDetected": "Zuletzt erkannte Spiele", "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"
} }

View file

@ -27,7 +27,7 @@
"settings-transparency-universal": "Universal", "settings-transparency-universal": "Universal",
"settings-transparency-modern": "Modern", "settings-transparency-modern": "Modern",
"settings-theme-transparent": "Transparent", "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": "Window Material",
"settings-material-desc": "Set the Windows background material Legcord uses.", "settings-material-desc": "Set the Windows background material Legcord uses.",
"settings-material-mica": "Mica", "settings-material-mica": "Mica",
@ -57,20 +57,13 @@
"settings-openCustomIconDialog": "Set desktop icon", "settings-openCustomIconDialog": "Set desktop icon",
"settings-mintoTray": "Work in background", "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-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": "Startup window", "settings-startMinimized": "Start minimized",
"settings-startMinimized-desc": "Choose how Legcord appears when it launches. Tray mode hides the window completely (needs a tray icon enabled).", "settings-startMinimized-desc": "Legcord starts in background and remains out of your way.",
"settings-startMinimized-off": "Normal",
"settings-startMinimized-minimized": "Minimized to taskbar",
"settings-startMinimized-tray": "Hidden in tray",
"settings-csp": "Content Security Policy", "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-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-strict": "Strict",
"settings-csp-none": "None", "settings-csp-none": "None",
"settings-csp-vanilla": "Vanilla", "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": "Use system CSS editor",
"settings-useSystemCssEditor-desc": "Use system CSS editor to edit CSS.", "settings-useSystemCssEditor-desc": "Use system CSS editor to edit CSS.",
"settings-MultiInstance": "Multi Instance", "settings-MultiInstance": "Multi Instance",
@ -81,14 +74,12 @@
"settings-automaticClientUpdates-desc": "Disables automatic Legcord updates", "settings-automaticClientUpdates-desc": "Disables automatic Legcord updates",
"settings-hardwareAcceleration": "Hardware acceleration", "settings-hardwareAcceleration": "Hardware acceleration",
"settings-hardwareAcceleration-desc": "Hardware acceleration uses your GPU to make Legcord run faster. If you're experiencing visual glitches, try disabling this.", "settings-hardwareAcceleration-desc": "Hardware acceleration uses your GPU to make Legcord run faster. If you're experiencing visual glitches, try disabling this.",
"settings-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": "Process scanning",
"settings-processScanning-desc": "Scan for running games to improve Rich Presence detection. Disable to reduce CPU usage. Requires a restart.", "settings-processScanning-desc": "Enables scanning for running games to improve Rich Presence detection.",
"settings-windowsLegacyScanning": "Windows legacy scanning", "settings-windowsLegacyScanning": "Windows legacy scanning",
"settings-windowsLegacyScanning-desc": "Uses legacy method for scanning processes on Windows (pre v1.1.6). May improve compatibility on some systems but is less efficient. Requires a restart.", "settings-windowsLegacyScanning-desc": "Uses legacy method for scanning processes on Windows (pre v1.1.6). May improve compatibility on some systems but is less efficient.",
"settings-scanInterval": "Scan interval (ms)", "settings-scanInterval": "Scan interval (ms)",
"settings-scanInterval-desc": "How often (in milliseconds) process scanning runs. Lower values detect games faster but use more CPU. Minimum 1000. Requires a restart.", "settings-scanInterval-desc": "Sets how often (in milliseconds) the process scanning occurs. Lower values may improve detection speed but can increase CPU usage.",
"settings-blockPowerSavingInVoiceChat": "Block power saving in voice chat", "settings-blockPowerSavingInVoiceChat": "Block power saving in voice chat",
"settings-blockPowerSavingInVoiceChat-desc": "Prevent Legcord from being suspended. Keeps system active but allows screen to be turned off.", "settings-blockPowerSavingInVoiceChat-desc": "Prevent Legcord from being suspended. Keeps system active but allows screen to be turned off.",
"settings-mobileMode": "Mobile mode", "settings-mobileMode": "Mobile mode",
@ -96,7 +87,7 @@
"settings-spellcheck": "Spellcheck", "settings-spellcheck": "Spellcheck",
"settings-spellcheck-desc": "Helps you correct misspelled words by highlighting them.", "settings-spellcheck-desc": "Helps you correct misspelled words by highlighting them.",
"settings-vaapi": "VAAPI", "settings-vaapi": "VAAPI",
"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-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-channel": "Discord channel", "settings-channel": "Discord channel",
"settings-channel-desc": "Use this setting to change current instance of Discord that Legcord is running.", "settings-channel-desc": "Use this setting to change current instance of Discord that Legcord is running.",
"settings-bitrateMin": "Minimum bitrate", "settings-bitrateMin": "Minimum bitrate",
@ -118,14 +109,9 @@
"settings-mod-vencord": "Lightweight, and easy to use client mod. Features a built-in store for plugins.", "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-mod-equicord": "Forked and born from vencord contributors, featuring a pretty plugin-rich client.",
"settings-prfmMode": "Performance mode", "settings-prfmMode": "Performance mode",
"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-desc": "Performance Mode is an experimental feature in Legcord designed to optimize responsiveness and performance based on your needs. The impact may vary depending on your hardware and usage, so we encourage you to try each mode to determine which works best for you.",
"settings-prfmMode-performance": "Performance", "settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Balanced",
"settings-prfmMode-battery": "Battery", "settings-prfmMode-battery": "Battery",
"settings-prfmMode-memory": "Memory saver",
"settings-prfmMode-voip": "Voice & video",
"settings-prfmMode-latency": "Low latency",
"settings-prfmMode-smoothScreenshare": "Smooth screenshare",
"settings-prfmMode-dynamic": "Dynamic", "settings-prfmMode-dynamic": "Dynamic",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Disable autogain", "settings-disableAutogain": "Disable autogain",
@ -152,14 +138,11 @@
"settings-save": "Save Settings", "settings-save": "Save Settings",
"settings-experimental": "Experimental", "settings-experimental": "Experimental",
"settings-restart": "Restart App", "settings-restart": "Restart App",
"invite-goBackToApp": "Go back to the Discord app",
"settings-updater": "Check for updates", "settings-updater": "Check for updates",
"settings-skipSplash": "Skip Splash Screen", "settings-skipSplash": "Skip Splash Screen",
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.", "settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
"settings-copyDebugInfo": "Copy Debug Info", "settings-copyDebugInfo": "Copy Debug Info",
"settings-copyGPUInfo": "Copy GPU Info", "settings-copyGPUInfo": "Copy GPU Info",
"settings-openWebRTCInternals": "Open WebRTC Internals",
"settings-openGPUInfo": "Open GPU Info",
"settings-clearClientModCache": "Clear client mod cache", "settings-clearClientModCache": "Clear client mod cache",
"settings-forceNativeCrash": "Force native crash", "settings-forceNativeCrash": "Force native crash",
"settings-smoothScroll": "Use smooth scrolling", "settings-smoothScroll": "Use smooth scrolling",
@ -169,39 +152,10 @@
"settings-quickCss": "Quick CSS", "settings-quickCss": "Quick CSS",
"settings-quickCss-desc": "Quickly edit your CSS in a simple text editor. Changes are applied immediately after saving the file.", "settings-quickCss-desc": "Quickly edit your CSS in a simple text editor. Changes are applied immediately after saving the file.",
"settings-category-lookAndFeel": "Look and feel", "settings-category-lookAndFeel": "Look and feel",
"settings-category-lookAndFeel-desc": "Window chrome, tray icon, splash screen, and mobile layout.",
"settings-category-mods": "Mods", "settings-category-mods": "Mods",
"settings-category-mods-desc": "Client mods, CSP, and plugin capabilities.",
"settings-category-behaviour": "Behaviour", "settings-category-behaviour": "Behaviour",
"settings-category-behaviour-desc": "Startup, tray, scrolling, spellcheck, and Discord channel.",
"settings-category-legacy": "Legacy features", "settings-category-legacy": "Legacy features",
"settings-category-networking": "Networking",
"settings-category-networking-desc": "HTTP(S) and SOCKS proxy settings for Discord and downloads.",
"settings-category-debug": "Debug options", "settings-category-debug": "Debug options",
"settings-category-powerManagement": "Power Management",
"settings-category-powerManagement-desc": "Performance presets and power-saving behaviour.",
"settings-category-arrpc": "Rich Presence",
"settings-category-arrpc-desc": "arRPC game activity detection and scanning.",
"settings-category-backup": "Backup and restore",
"settings-category-backup-desc": "Export or restore settings, themes, plugins, and mod data.",
"settings-category-advanced": "Advanced",
"settings-category-advanced-desc": "Audio capture, hardware, updates, flags, and debug tools.",
"settings-search": "Search settings",
"settings-search-desc": "Filter settings by name or description.",
"settings-noResults": "No settings match your search.",
"settings-proxyMode": "Proxy mode",
"settings-proxyMode-desc": "Choose how Legcord connects through a proxy. System uses your OS proxy (and HTTP(S)_PROXY environment variables). Fixed servers and PAC work like in a normal web browser. A restart is required.",
"settings-proxyMode-system": "System",
"settings-proxyMode-direct": "Direct (no proxy)",
"settings-proxyMode-fixed_servers": "Fixed servers",
"settings-proxyMode-pac_script": "PAC script",
"settings-proxyMode-auto_detect": "Auto-detect",
"settings-proxyRules": "Proxy server",
"settings-proxyRules-desc": "Proxy URL or Chromium proxy rules. Examples: http://127.0.0.1:8080, socks5://127.0.0.1:1080, or http=proxy:80;https=proxy:80",
"settings-proxyPacScript": "PAC script URL",
"settings-proxyPacScript-desc": "URL of a proxy auto-config (PAC) file, e.g. http://wpad/wpad.dat or file:///path/to/proxy.pac",
"settings-proxyBypassRules": "Proxy bypass list",
"settings-proxyBypassRules-desc": "Comma-separated hosts that skip the proxy. Use <local> for localhost. Example: <local>,*.intranet.example,10.0.0.0/8",
"menu-about": "About Legcord", "menu-about": "About Legcord",
"menu-developerTools": "Developer tools", "menu-developerTools": "Developer tools",
"menu-openSettings": "Open settings", "menu-openSettings": "Open settings",
@ -249,13 +203,10 @@
"setup-welcomeTitle": "Welcome to Legcord", "setup-welcomeTitle": "Welcome to Legcord",
"setup-welcomeSubtitle": "Let's get you set up with your perfect configuration.", "setup-welcomeSubtitle": "Let's get you set up with your perfect configuration.",
"setup-getStarted": "Get Started", "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-nativeTitle": "Native Window",
"setup-windowStyle-nativeDesc": "Use your system's default window decorations", "setup-windowStyle-nativeDesc": "Use your system's default window decorations",
"setup-windowStyle-customTitle": "Custom Titlebar", "setup-windowStyle-customTitle": "Custom Titlebar",
"setup-windowStyle-customDesc": "Use Legcord's custom titlebar design", "setup-windowStyle-customDesc": "Use Legcord's custom titlebar design",
"setup-recommended": "Recommended",
"setup-chooseWindowStyle": "Choose Window Style", "setup-chooseWindowStyle": "Choose Window Style",
"setup-selectAppearance": "Select how Legcord appears on your machine", "setup-selectAppearance": "Select how Legcord appears on your machine",
"setup-systemTray": "System Tray", "setup-systemTray": "System Tray",
@ -271,8 +222,6 @@
"setup-launchLegcord": "Launch Legcord", "setup-launchLegcord": "Launch Legcord",
"setup-modSelectorTitle": "Choose Your Client Mod", "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-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-vencordTitle": "Vencord",
"setup-vencordDesc": "Client mod with plugins and themes.", "setup-vencordDesc": "Client mod with plugins and themes.",
"setup-equicordTitle": "Equicord", "setup-equicordTitle": "Equicord",
@ -290,6 +239,8 @@
"settings-channel-stable": "Stable", "settings-channel-stable": "Stable",
"settings-channel-canary": "Canary", "settings-channel-canary": "Canary",
"settings-channel-ptb": "PTB", "settings-channel-ptb": "PTB",
"settings-category-powerManagement": "Power Management",
"settings-category-arrpc": "arRPC",
"settings-extendedPluginAbilities": "Extended plugin abilities", "settings-extendedPluginAbilities": "Extended plugin abilities",
"settings-extendedPluginAbilities-desc": "Allows plugins to read and write files in a scoped folder on your computer (e.g. for caching deleted messages). Only enable for plugins you trust—they can store data locally. Data is stored per plugin in Legcord's plugin-storage folder.", "settings-extendedPluginAbilities-desc": "Allows plugins to read and write files in a scoped folder on your computer (e.g. for caching deleted messages). Only enable for plugins you trust—they can store data locally. Data is stored per plugin in Legcord's plugin-storage folder.",
"settings-audio-loopback": "Loopback", "settings-audio-loopback": "Loopback",
@ -308,7 +259,6 @@
"keybind-navigateBack": "Navigate back", "keybind-navigateBack": "Navigate back",
"keybind-runJavascript": "Run Javascript", "keybind-runJavascript": "Run Javascript",
"keybind-openQuickCss": "Open Quick CSS", "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-globalNote": "Allows you to assign a specific keyboard shortcut that can be used across different applications and programs.",
"keybind-global": "Global", "keybind-global": "Global",
"keybind-enabled": "Enabled", "keybind-enabled": "Enabled",
@ -321,21 +271,12 @@
"detectable-appName": "App Name*", "detectable-appName": "App Name*",
"detectable-appId": "App ID*", "detectable-appId": "App ID*",
"detectable-themes": "Themes", "detectable-themes": "Themes",
"detectable-themes-note": "Optional genre tags for this game, comma-separated.",
"detectable-aliases": "Aliases", "detectable-aliases": "Aliases",
"detectable-aliases-note": "Optional alternate names Discord may match, comma-separated.",
"detectable-appId-note": "Discord application ID from the Developer Portal for this games rich presence.",
"detectable-enabled": "Enabled", "detectable-enabled": "Enabled",
"detectable-placeholderName": "e.g. Discord", "detectable-placeholderName": "e.g. Discord",
"detectable-placeholderId": "e.g. 1234567890", "detectable-placeholderId": "e.g. 1234567890",
"detectable-placeholderThemes": "Action, Adventure", "detectable-placeholderThemes": "Action, Adventure",
"detectable-placeholderAliases": "Alias1, Alias2", "detectable-placeholderAliases": "Alias1, Alias2",
"themes-pageTitle": "Themes",
"themes-pageDesc": "Install BetterDiscord themes or write Quick CSS to customize Discords look. Themes are .theme.css files; Quick CSS is a single stylesheet you can edit anytime.",
"themes-empty": "No themes installed yet.",
"themes-emptyDesc": "Import a .theme.css file or paste a raw theme URL below. You can also browse community themes online.",
"themes-browseThemes": "Browse themes on BetterDiscord",
"themes-importUrlNote": "Paste a direct link to a raw .theme.css file (for example from GitHub).",
"themes-success": "Success!", "themes-success": "Success!",
"themes-updated": "Theme successfully updated!", "themes-updated": "Theme successfully updated!",
"themes-bdInstalled": "BD theme successfully installed!", "themes-bdInstalled": "BD theme successfully installed!",
@ -348,48 +289,22 @@
"themes-importFromFile": "Import from file", "themes-importFromFile": "Import from file",
"themes-openThemesFolder": "Open themes folder", "themes-openThemesFolder": "Open themes folder",
"themes-import": "Import", "themes-import": "Import",
"themes-refresh": "Refresh",
"themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]", "themes-importUrlPlaceholder": "https://raw.githubusercontent.com/... [.theme.css]",
"themes-installed": "Installed Themes",
"keybinds-pageTitle": "Keybinds",
"keybinds-pageDesc": "Create keyboard shortcuts for Legcord actions like mute, deafen, or opening Quick CSS. Enable Global on a keybind to use it even when Discord is in the background.",
"keybinds-empty": "No keybinds added yet.",
"keybinds-emptyDesc": "Start by adding one with the button above.",
"games-registeredGames": "Registered Games", "games-registeredGames": "Registered Games",
"games-pageDesc": "Manage custom games for Rich Presence (arRPC). Pick a running process, then set its Discord application ID. Changes usually need a restart to apply.",
"games-refreshList": "Refresh list", "games-refreshList": "Refresh list",
"games-add": "Add", "games-add": "Add",
"games-remove": "Remove", "games-remove": "Remove",
"games-removeConfirmHeader": "Remove game?", "games-removeConfirmHeader": "Remove game?",
"games-removeConfirmBody": "This game will be removed from rich presence. The client must be restarted for changes to take effect.", "games-removeConfirmBody": "This game will be removed from rich presence. The client must be restarted for changes to take effect.",
"games-noExecutables": "No executables", "games-noExecutables": "No executables",
"games-empty": "No registered games yet.", "games-empty": "No registered games. Add one using the dropdown above.",
"games-emptyDesc": "Choose a process from the dropdown above, then click Add to register it for rich presence.",
"games-lastDetected": "Last detected games", "games-lastDetected": "Last detected games",
"games-lastDetectedEmpty": "No games detected yet.", "games-lastDetectedEmpty": "No games detected yet. Start a game with rich presence to see them here.",
"games-lastDetectedEmptyDesc": "Start a game with rich presence while Legcord is running to see it listed here.",
"games-blacklist": "Blacklist", "games-blacklist": "Blacklist",
"games-blacklisted": "Blacklisted games", "games-blacklisted": "Blacklisted games",
"games-blacklistedEmpty": "No blacklisted games.", "games-blacklistedEmpty": "No blacklisted games.",
"games-blacklistedEmptyDesc": "Games you blacklist wont show up in last detected.",
"games-removeFromBlacklist": "Remove", "games-removeFromBlacklist": "Remove",
"games-application": "Application", "games-application": "Application",
"plugins-pageTitle": "Plugins",
"plugins-pageDesc": "Experimental filesystem plugins loaded from your Legcord plugins folder. Each plugin can run code in the main process, preload, and/or renderer. Only enable plugins you trust.",
"plugins-empty": "No runtime plugins found.",
"plugins-emptyDesc": "Open the plugins folder and add a plugin directory with a manifest.json, then refresh this list.",
"plugins-refresh": "Refresh list",
"plugins-openFolder": "Open plugins folder",
"plugins-reload": "Reload",
"plugins-targets": "Targets",
"plugins-targetsNone": "none",
"plugins-incompatible": "Plugin is not compatible.",
"plugins-toastTitle": "Plugins",
"plugins-toastEnableFailed": "Failed to enable {name}.",
"plugins-toastDisableFailed": "Failed to disable {name}.",
"plugins-toastReloaded": "Reloaded {name}.",
"plugins-toastReloadFailed": "Failed to reload {name}.",
"plugins-toastIncompatible": "{name} is not compatible with this Legcord version.",
"screenshare-selectSource": "Please select a source", "screenshare-selectSource": "Please select a source",
"screenshare-venmicDisabled": "Venmic disabled", "screenshare-venmicDisabled": "Venmic disabled",
"screenshare-share": "Share", "screenshare-share": "Share",

View file

@ -27,7 +27,7 @@
"settings-transparency-universal": "Universal", "settings-transparency-universal": "Universal",
"settings-transparency-modern": "Moderna", "settings-transparency-modern": "Moderna",
"settings-theme-transparent": "Transparente", "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": "Ventana emergente siempre visible",
"settings-popoutPiP-desc": "Cuanto está habilitado, la ventana emergente estará en modo siempre visible.", "settings-popoutPiP-desc": "Cuanto está habilitado, la ventana emergente estará en modo siempre visible.",
"settings-venmic-workaround": "Workaraound", "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-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": "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-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": "Ventana al iniciar", "settings-startMinimized": "Iniciar minimizado",
"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-startMinimized-desc": "Legcord se inicia en segundo plano y no interfiere en tu trabajo.",
"settings-useSystemCssEditor": "Utilizar el editor CSS del sistema", "settings-useSystemCssEditor": "Utilizar el editor CSS del sistema",
"settings-useSystemCssEditor-desc": "Utilice el editor CSS del sistema para editar CSS.", "settings-useSystemCssEditor-desc": "Utilice el editor CSS del sistema para editar CSS.",
"settings-MultiInstance": "Múltiples instancias", "settings-MultiInstance": "Múltiples instancias",
@ -95,12 +95,7 @@
"settings-prfmMode": "Modo rendimiento", "settings-prfmMode": "Modo rendimiento",
"settings-prfmMode-desc": "El modo de rendimiento es una función experimental de Legcord diseñada para optimizar la capacidad de respuesta y el rendimiento en función de tus necesidades. El impacto puede variar en función de tu hardware y uso, por lo que te recomendamos que pruebes cada modo para determinar cuál te conviene más.", "settings-prfmMode-desc": "El modo de rendimiento es una función experimental de Legcord diseñada para optimizar la capacidad de respuesta y el rendimiento en función de tus necesidades. El impacto puede variar en función de tu hardware y uso, por lo que te recomendamos que pruebes cada modo para determinar cuál te conviene más.",
"settings-prfmMode-performance": "Rendimiento", "settings-prfmMode-performance": "Rendimiento",
"settings-prfmMode-balanced": "Equilibrado",
"settings-prfmMode-battery": "Batería", "settings-prfmMode-battery": "Batería",
"settings-prfmMode-memory": "Ahorro de memoria",
"settings-prfmMode-voip": "Voz y vídeo",
"settings-prfmMode-latency": "Baja latencia",
"settings-prfmMode-smoothScreenshare": "Compartir pantalla fluido",
"settings-prfmMode-dynamic": "Dinámico", "settings-prfmMode-dynamic": "Dinámico",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Desactivar ganancia automática", "settings-disableAutogain": "Desactivar ganancia automática",
@ -303,9 +298,5 @@
"games-removeFromBlacklist": "Eliminar", "games-removeFromBlacklist": "Eliminar",
"games-application": "Aplicación", "games-application": "Aplicación",
"settings-vaapi": "VAAPI", "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"
} }

View file

@ -19,7 +19,7 @@
"settings-transparency-universal": "Universel", "settings-transparency-universal": "Universel",
"settings-transparency-modern": "Moderne", "settings-transparency-modern": "Moderne",
"settings-theme-transparent": "Transparent", "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": "Sélection du périphérique",
"settings-venmic-deviceSelect-desc": "Autoriser la sélection d'un périphérique audio.", "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.", "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-loopback": "Loopback",
"settings-audio-loopbackWithMute": "Loopback avec sourdine", "settings-audio-loopbackWithMute": "Loopback avec sourdine",
"settings-openCustomIconDialog": "Définir une icône de bureau", "settings-openCustomIconDialog": "Définir une icône de bureau",
"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-startMinimized-desc": "Legcord démarre en arrière-plan sans vous déranger.",
"settings-useSystemCssEditor": "Utiliser l'éditeur CSS du système", "settings-useSystemCssEditor": "Utiliser l'éditeur CSS du système",
"settings-useSystemCssEditor-desc": "Utiliser l'éditeur CSS du système pour modifier le CSS.", "settings-useSystemCssEditor-desc": "Utiliser l'éditeur CSS du système pour modifier le CSS.",
"settings-MultiInstance": "Multi-instances", "settings-MultiInstance": "Multi-instances",
@ -57,12 +57,7 @@
"settings-prfmMode": "Mode performance", "settings-prfmMode": "Mode performance",
"settings-prfmMode-desc": "Le mode performance est une fonctionnalité expérimentale de Legcord conçue pour optimiser la réactivité et les performances selon vos besoins. Son impact peut varier selon votre matériel et votre utilisation, c'est pourquoi nous vous encourageons à essayer chaque mode pour déterminer lequel vous correspond le mieux.", "settings-prfmMode-desc": "Le mode performance est une fonctionnalité expérimentale de Legcord conçue pour optimiser la réactivité et les performances selon vos besoins. Son impact peut varier selon votre matériel et votre utilisation, c'est pourquoi nous vous encourageons à essayer chaque mode pour déterminer lequel vous correspond le mieux.",
"settings-prfmMode-performance": "Performance", "settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Équilibré",
"settings-prfmMode-battery": "Batterie", "settings-prfmMode-battery": "Batterie",
"settings-prfmMode-memory": "Économie mémoire",
"settings-prfmMode-voip": "Voix et vidéo",
"settings-prfmMode-latency": "Faible latence",
"settings-prfmMode-smoothScreenshare": "Partage d'écran fluide",
"settings-prfmMode-dynamic": "Dynamique", "settings-prfmMode-dynamic": "Dynamique",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableHttpCache": "Désactiver le cache HTTP", "settings-disableHttpCache": "Désactiver le cache HTTP",
@ -109,7 +104,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-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": "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-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": "Fenêtre au démarrage", "settings-startMinimized": "Démarrer en mode minimisé",
"settings-noBundleUpdates": "Pas de mises à jour", "settings-noBundleUpdates": "Pas de mises à jour",
"settings-noBundleUpdates-desc": "Désactive les mises à jour automatique des mods.", "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.", "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.",
@ -344,8 +339,5 @@
"supportBanner-title": "Soutenir le projet", "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-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", "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"
} }

View file

@ -27,7 +27,7 @@
"settings-theme-desc": "Az ablakstílus kezeli, hogy a Legcord milyen fejlécet használ.", "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-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-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": "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-popoutPiP-desc": "Ha engedélyezve van, a hívás felugró ablaka mindig felül lesz.",
"settings-venmic-workaround": "Ideiglenes megoldás", "settings-venmic-workaround": "Ideiglenes megoldás",
@ -48,11 +48,11 @@
"settings-audio": "Hang", "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-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-openCustomIconDialog": "Asztali ikon beállítása",
"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-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-mintoTray": "Futás a háttérben", "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-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": "Induló ablak", "settings-startMinimized": "Kisablakos indítás",
"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-startMinimized-desc": "A Legcord a háttérben indul el, és nem zavar.",
"settings-useSystemCssEditor": "Használja a rendszer CSS-szerkesztőjét", "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-useSystemCssEditor-desc": "A CSS szerkesztéséhez használja a rendszer CSS-szerkesztőjét.",
"settings-MultiInstance": "Több példány", "settings-MultiInstance": "Több példány",
@ -83,8 +83,8 @@
"settings-bitrateTarget-desc": "Cél bitráta képernyőmegosztáshoz.", "settings-bitrateTarget-desc": "Cél bitráta képernyőmegosztáshoz.",
"settings-invitewebsocket": "Rich Presence", "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-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": "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-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": "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-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", "settings-mod": "Kliens bővítmény",
@ -94,12 +94,7 @@
"settings-prfmMode": "Teljesítmény mód", "settings-prfmMode": "Teljesítmény mód",
"settings-prfmMode-desc": "A Teljesítmény mód egy kísérleti funkció a Legcordban, melynek célja a válaszidő és a teljesítmény optimalizálása az Ön igényei alapján. A hatás a hardvertől és a használattól függően változhat, ezért javasoljuk, hogy próbálja ki az egyes módokat, hogy megállapítsa, melyik működik legjobban az Ön számára.", "settings-prfmMode-desc": "A Teljesítmény mód egy kísérleti funkció a Legcordban, melynek célja a válaszidő és a teljesítmény optimalizálása az Ön igényei alapján. A hatás a hardvertől és a használattól függően változhat, ezért javasoljuk, hogy próbálja ki az egyes módokat, hogy megállapítsa, melyik működik legjobban az Ön számára.",
"settings-prfmMode-performance": "Teljesítmény", "settings-prfmMode-performance": "Teljesítmény",
"settings-prfmMode-balanced": "Kiegyensúlyozott",
"settings-prfmMode-battery": "Akkumulátor", "settings-prfmMode-battery": "Akkumulátor",
"settings-prfmMode-memory": "Memóriatakarékos",
"settings-prfmMode-voip": "Hang és videó",
"settings-prfmMode-latency": "Alacsony késleltetés",
"settings-prfmMode-smoothScreenshare": "Sima képernyőmegosztás",
"settings-prfmMode-dynamic": "Dinamikus", "settings-prfmMode-dynamic": "Dinamikus",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Automatikus erősítés letiltása", "settings-disableAutogain": "Automatikus erősítés letiltása",
@ -295,9 +290,9 @@
"touchbar-servers": "Szerverek", "touchbar-servers": "Szerverek",
"splash-title": "Legcord", "splash-title": "Legcord",
"settings-venmic-granularSelect": "Részletes kiválasztás", "settings-venmic-granularSelect": "Részletes kiválasztás",
"settings-trayIcon-white-plug-alt": "Fehér csatlakozó alternatíva", "settings-trayIcon-white-plug-alt": "Fehér Plug alternatíva",
"settings-trayIcon-black-plug": "Fekete csatlakozó", "settings-trayIcon-black-plug": "Fekete Plug",
"settings-trayIcon-black-plug-alt": "Fekete csatlakozó alternatíva", "settings-trayIcon-black-plug-alt": "Fekete Plug alternatíva",
"menu-redo": "Újra", "menu-redo": "Újra",
"setup-stepOf": "{current} / {total} lépés", "setup-stepOf": "{current} / {total} lépés",
"keybind-deafen": "Süketítés", "keybind-deafen": "Süketítés",
@ -344,15 +339,5 @@
"settings-material-mica": "Mica", "settings-material-mica": "Mica",
"settings-material-mica-alt": "Mica Alt", "settings-material-mica-alt": "Mica Alt",
"settings-material-acrylic": "Akril", "settings-material-acrylic": "Akril",
"settings-material-none": "Egyik sem", "settings-material-none": "None"
"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"
} }

View file

@ -27,7 +27,7 @@
"settings-transparency-universal": "Universal", "settings-transparency-universal": "Universal",
"settings-transparency-modern": "Modern", "settings-transparency-modern": "Modern",
"settings-theme-transparent": "Transparan", "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": "Panggilan Popout Selalu di Atas",
"settings-popoutPiP-desc": "Saat diaktifkan, jendela popout panggilan akan berada dalam mode Selalu di Atas.", "settings-popoutPiP-desc": "Saat diaktifkan, jendela popout panggilan akan berada dalam mode Selalu di Atas.",
"settings-venmic-workaround": "Solusi sementara", "settings-venmic-workaround": "Solusi sementara",
@ -49,11 +49,11 @@
"settings-audio": "Audio", "settings-audio": "Audio",
"settings-audio-desc": "Pilih metode yang digunakan Legcord untuk mengambil audio dari sistem Anda selama berbagi layar.", "settings-audio-desc": "Pilih metode yang digunakan Legcord untuk mengambil audio dari sistem Anda selama berbagi layar.",
"settings-openCustomIconDialog": "Atur ikon desktop", "settings-openCustomIconDialog": "Atur ikon desktop",
"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-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-mintoTray": "Bekerja di latar belakang", "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-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": "Jendela saat mulai", "settings-startMinimized": "Mulai dalam mode minimized",
"settings-startMinimized-desc": "Pilih bagaimana Legcord muncul saat diluncurkan. Mode tray menyembunyikan jendela sepenuhnya (ikon tray harus diaktifkan).", "settings-startMinimized-desc": "Legcord berjalan di latar belakang dan tidak mengganggu Anda.",
"settings-useSystemCssEditor": "Gunakan editor CSS sistem", "settings-useSystemCssEditor": "Gunakan editor CSS sistem",
"settings-useSystemCssEditor-desc": "Gunakan editor CSS sistem untuk mengedit CSS.", "settings-useSystemCssEditor-desc": "Gunakan editor CSS sistem untuk mengedit CSS.",
"settings-MultiInstance": "Multi Instans", "settings-MultiInstance": "Multi Instans",
@ -89,12 +89,7 @@
"settings-prfmMode": "Mode kinerja", "settings-prfmMode": "Mode kinerja",
"settings-prfmMode-desc": "Mode Kinerja adalah fitur eksperimental di Legcord yang dirancang untuk mengoptimalkan responsivitas dan kinerja sesuai dengan kebutuhan Anda. Dampak yang dihasilkan dapat bervariasi tergantung pada spesifikasi perangkat keras dan pola penggunaan Anda, jadi kami menyarankan Anda untuk mencoba setiap mode untuk menentukan mana yang paling sesuai dengan kebutuhan Anda.", "settings-prfmMode-desc": "Mode Kinerja adalah fitur eksperimental di Legcord yang dirancang untuk mengoptimalkan responsivitas dan kinerja sesuai dengan kebutuhan Anda. Dampak yang dihasilkan dapat bervariasi tergantung pada spesifikasi perangkat keras dan pola penggunaan Anda, jadi kami menyarankan Anda untuk mencoba setiap mode untuk menentukan mana yang paling sesuai dengan kebutuhan Anda.",
"settings-prfmMode-performance": "Kinerja", "settings-prfmMode-performance": "Kinerja",
"settings-prfmMode-balanced": "Seimbang",
"settings-prfmMode-battery": "Baterai", "settings-prfmMode-battery": "Baterai",
"settings-prfmMode-memory": "Penghemat memori",
"settings-prfmMode-voip": "Suara & video",
"settings-prfmMode-latency": "Latensi rendah",
"settings-prfmMode-smoothScreenshare": "Berbagi layar mulus",
"settings-prfmMode-dynamic": "Dinamis", "settings-prfmMode-dynamic": "Dinamis",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Nonaktifkan autogain", "settings-disableAutogain": "Nonaktifkan autogain",
@ -291,65 +286,5 @@
"contextMenu-searchGoogle": "Cari dengan Google", "contextMenu-searchGoogle": "Cari dengan Google",
"contextMenu-searchDuckDuckGo": "Cari dengan DuckDuckGo", "contextMenu-searchDuckDuckGo": "Cari dengan DuckDuckGo",
"touchbar-servers": "Server", "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"
} }

View file

@ -31,6 +31,6 @@
"settings-transparency-desc": "Legcordの透明化モードを設定します。", "settings-transparency-desc": "Legcordの透明化モードを設定します。",
"settings-transparency-universal": "ユニバーサル", "settings-transparency-universal": "ユニバーサル",
"settings-transparency-modern": "モダン", "settings-transparency-modern": "モダン",
"settings-transparency-tahoe-warning": "macOS 26 Tahoeにおいて、透明化は過度なラグを引き起こす可能性があります。", "settings-transparency-tahoe-warning": "MacOS 26 Tahoeにおいて、透明化は過度なラグを引き起こす可能性があります。",
"detectable-themes": "テーマ" "detectable-themes": "テーマ"
} }

View file

@ -31,7 +31,7 @@
"settings-sleepInBackground": "Descansar em segundo plano", "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-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-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": "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-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", "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-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": "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-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": "Janela ao iniciar", "settings-startMinimized": "Abrir minimizado",
"settings-startMinimized-desc": "Escolha como o Legcord aparece ao iniciar. O modo bandeja oculta a janela por completo (é preciso o ícone da bandeja).", "settings-startMinimized-desc": "Legcord inicia em segundo plano e fica fora do seu caminho.",
"settings-useSystemCssEditor": "Usar editor de estilo (CSS) do sistema", "settings-useSystemCssEditor": "Usar editor de estilo (CSS) do sistema",
"settings-useSystemCssEditor-desc": "Usar editor de estilo padrão do sistema para editar CSS.", "settings-useSystemCssEditor-desc": "Usar editor de estilo padrão do sistema para editar CSS.",
"settings-MultiInstance": "Múltiplas instâncias", "settings-MultiInstance": "Múltiplas instâncias",
@ -145,12 +145,7 @@
"settings-prfmMode": "Modo de performance", "settings-prfmMode": "Modo de performance",
"settings-prfmMode-desc": "Modo de performance é uma funcionalidade experimental do Legcord feita para otimizar responsitividade e performance baseado nas suas nescessidades. Impacto pode variar dependendo da configuração da sua máquina e seu uso, então recomendamos tentar cada modo e escolher um que sirva melhor para você.", "settings-prfmMode-desc": "Modo de performance é uma funcionalidade experimental do Legcord feita para otimizar responsitividade e performance baseado nas suas nescessidades. Impacto pode variar dependendo da configuração da sua máquina e seu uso, então recomendamos tentar cada modo e escolher um que sirva melhor para você.",
"settings-prfmMode-performance": "Performance", "settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Equilibrado",
"settings-prfmMode-battery": "Bateria", "settings-prfmMode-battery": "Bateria",
"settings-prfmMode-memory": "Economia de memória",
"settings-prfmMode-voip": "Voz e vídeo",
"settings-prfmMode-latency": "Baixa latência",
"settings-prfmMode-smoothScreenshare": "Compartilhamento de tela suave",
"settings-prfmMode-dynamic": "Dinâmico", "settings-prfmMode-dynamic": "Dinâmico",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Desabilitar ganho automático", "settings-disableAutogain": "Desabilitar ganho automático",
@ -250,8 +245,5 @@
"setup-trayEnableDesc": "Mostrar o Legcord na bandeja do sistema", "setup-trayEnableDesc": "Mostrar o Legcord na bandeja do sistema",
"setup-trayDisableTitle": "Desabilitar ícone de bandeja", "setup-trayDisableTitle": "Desabilitar ícone de bandeja",
"setup-trayDisableDesc": "Não mostrar o Legcord na bandeja do sistema", "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"
} }

View file

@ -25,7 +25,7 @@
"settings-transparency-universal": "Universal", "settings-transparency-universal": "Universal",
"settings-transparency-modern": "Moderno", "settings-transparency-modern": "Moderno",
"settings-theme-transparent": "Transparente", "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-popoutPiP": "Elevação de Chamada sempre acima",
"settings-venmic-workaround": "Solução alternativa/temporária", "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.", "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,12 +45,7 @@
"settings-audio-desc": "Selecione o método que o Legcord usa para capturar o áudio do seu sistema durante o partilhamento de ecrã.", "settings-audio-desc": "Selecione o método que o Legcord usa para capturar o áudio do seu sistema durante o partilhamento de ecrã.",
"settings-prfmMode": "Modo de desempenho", "settings-prfmMode": "Modo de desempenho",
"settings-prfmMode-performance": "Desempenho", "settings-prfmMode-performance": "Desempenho",
"settings-prfmMode-balanced": "Equilibrado",
"settings-prfmMode-dynamic": "Dinâmico", "settings-prfmMode-dynamic": "Dinâmico",
"settings-prfmMode-latency": "Baixa latência",
"settings-prfmMode-memory": "Poupança de memória",
"settings-prfmMode-voip": "Voz e vídeo",
"settings-prfmMode-smoothScreenshare": "Partilha de ecrã fluida",
"settings-none": "Nenjum", "settings-none": "Nenjum",
"settings-save": "Gravar configurações", "settings-save": "Gravar configurações",
"settings-restart": "Reiniciar a app", "settings-restart": "Reiniciar a app",
@ -125,234 +120,5 @@
"contextMenu-searchGoogle": "Pesquisar com Google", "contextMenu-searchGoogle": "Pesquisar com Google",
"contextMenu-searchDuckDuckGo": "Pesquisar com DuckDuckGo", "contextMenu-searchDuckDuckGo": "Pesquisar com DuckDuckGo",
"touchbar-servers": "Servidores", "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"
} }

View file

@ -15,20 +15,20 @@
"setup_question5": "Хотите использовать значок в трее?", "setup_question5": "Хотите использовать значок в трее?",
"settings-theme": "Стиль окна", "settings-theme": "Стиль окна",
"settings-theme-desc": "Стиль окна определяет, какой заголовок использует Legcord.", "settings-theme-desc": "Стиль окна определяет, какой заголовок использует Legcord.",
"settings-theme-default": "По умолчанию (Legcord)", "settings-theme-default": "По умолчанию (настраиваемая)",
"settings-theme-native": "Системная", "settings-theme-native": "Нативная",
"settings-autoHideMenuBar": "Автоскрытие строки меню", "settings-autoHideMenuBar": "Авто-скрытие строки меню",
"settings-autoHideMenuBar-desc": "Автоскрытие строки меню, когда она не используется.", "settings-autoHideMenuBar-desc": "Авто-скрытие строки меню, когда она не используется.",
"settings-sleepInBackground": "Переходить в спящий режим в фоне", "settings-sleepInBackground": "Переходить в спящий режим в фоне",
"settings-sleepInBackground-desc": "Включает ограничение скорости фоновой работы Chromium. Может снизить энергопотребление, но приводит к некорректной работе уведомлений.", "settings-sleepInBackground-desc": "Включает ограничение скорости фоновой работы Chromium. Может снизить энергопотребление, но приводит к некорректной работе уведомлений.",
"settings-transparency": "Прозрачность", "settings-transparency": "Прозрачность",
"settings-transparency-desc": "Выберите режим прозрачности, который будет использовать Legcord.", "settings-transparency-desc": "Установите режим прозрачности Legcord.",
"settings-transparency-universal": "Универсальный", "settings-transparency-universal": "Универсальный",
"settings-transparency-modern": "Современный", "settings-transparency-modern": "Современный",
"settings-theme-transparent": "Прозрачный", "settings-theme-transparent": "Прозрачный",
"settings-transparency-tahoe-warning": "Прозрачность может вызывать чрезмерные лаги на macOS 26 Tahoe.", "settings-transparency-tahoe-warning": "Прозрачность может вызвать чрезмерные лаги на MacOS 26 Tahoe.",
"settings-popoutPiP": "Всплывающее окно вызова поверх всех окон", "settings-popoutPiP": "Всплывающее окно вызова всегда сверху",
"settings-popoutPiP-desc": "При включении этой функции всплывающее окно вызова всегда будет отображаться в режиме Поверх всех окон.", "settings-popoutPiP-desc": "При включении этой функции всплывающее окно вызова будет отображаться в режиме Всегда поверх других окон.",
"settings-venmic-deviceSelect": "Выбор устройства", "settings-venmic-deviceSelect": "Выбор устройства",
"settings-venmic-deviceSelect-desc": "Позволяет выбрать аудио устройство.", "settings-venmic-deviceSelect-desc": "Позволяет выбрать аудио устройство.",
"settings-venmic-granularSelect-desc": "Позволить выбрать источник аудиовхода.", "settings-venmic-granularSelect-desc": "Позволить выбрать источник аудиовхода.",
@ -39,190 +39,5 @@
"settings-venmic-ignoreInputMedia-desc": "Игнорировать аудио вход из медиаисточников.", "settings-venmic-ignoreInputMedia-desc": "Игнорировать аудио вход из медиаисточников.",
"settings-venmic-onlySpeakers": "Только динамики", "settings-venmic-onlySpeakers": "Только динамики",
"settings-venmic-onlySpeakers-desc": "Использовать только динамики для выбора аудиовыхода.", "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": "Скрыто в трее"
} }

View file

@ -1,8 +1 @@
{ {}
"loading_screen_start": "Legcord başlatılıyor…",
"loading_screen_update": "Legcordun yeni bir versiyonu var. Lütfen son versiyona güncelleyin.",
"setup_question1": "Legcord kurulumuna hoş geldiniz",
"yes": "Evet",
"no": "Hayır",
"next": "Sonraki"
}

View file

@ -27,7 +27,7 @@
"settings-transparency-universal": "Універсальний", "settings-transparency-universal": "Універсальний",
"settings-transparency-modern": "Сучасний", "settings-transparency-modern": "Сучасний",
"settings-theme-transparent": "Прозорий", "settings-theme-transparent": "Прозорий",
"settings-transparency-tahoe-warning": "Прозорість може призвести до надмірного лагу на macOS 26 Tahoe.", "settings-transparency-tahoe-warning": "Прозорість може призвести до надмірного лагу на MacOS 26 Tahoe.",
"settings-popoutPiP": "Виклик спливаючого вікна завжди зверху", "settings-popoutPiP": "Виклик спливаючого вікна завжди зверху",
"settings-popoutPiP-desc": "Коли цю функцію ввімкнено, спливаюче вікно виклику буде в режимі «Завжди зверху».", "settings-popoutPiP-desc": "Коли цю функцію ввімкнено, спливаюче вікно виклику буде в режимі «Завжди зверху».",
"settings-venmic-workaround": "Тимчасове вирішення", "settings-venmic-workaround": "Тимчасове вирішення",
@ -52,8 +52,8 @@
"settings-csp-desc": "Legcord CSP це наша система, яка керує завантаженням користувацького контенту в додаток Discord. Від неї залежать такі речі, як клієнтські моди та теми. Вимкніть, якщо хочете позбутися модів та користувацьких стилів.", "settings-csp-desc": "Legcord CSP це наша система, яка керує завантаженням користувацького контенту в додаток Discord. Від неї залежать такі речі, як клієнтські моди та теми. Вимкніть, якщо хочете позбутися модів та користувацьких стилів.",
"settings-mintoTray": "Робота у фоновому режимі", "settings-mintoTray": "Робота у фоновому режимі",
"settings-mintoTray-desc": "Якщо вимкнено, Legcord закриється, як і будь-яке інше вікно, якщо його закрити, інакше він залишиться в системному треї на потім.", "settings-mintoTray-desc": "Якщо вимкнено, Legcord закриється, як і будь-яке інше вікно, якщо його закрити, інакше він залишиться в системному треї на потім.",
"settings-startMinimized": "Вікно при запуску", "settings-startMinimized": "Почати згорнутий",
"settings-startMinimized-desc": "Як Legcord з’являється під час запуску. Режим трея повністю ховає вікно (потрібна іконка в треї).", "settings-startMinimized-desc": "Легкорд починається на задньому плані та залишається поза вашими перешкодами.",
"settings-useSystemCssEditor": "Використовуйте системний редактор CSS", "settings-useSystemCssEditor": "Використовуйте системний редактор CSS",
"settings-useSystemCssEditor-desc": "Використовуйте системний редактор CSS для редагування CSS.", "settings-useSystemCssEditor-desc": "Використовуйте системний редактор CSS для редагування CSS.",
"settings-MultiInstance": "Багато екземплярів", "settings-MultiInstance": "Багато екземплярів",
@ -89,12 +89,7 @@
"settings-prfmMode": "Режим продуктивності", "settings-prfmMode": "Режим продуктивності",
"settings-prfmMode-desc": "Режим продуктивності це експериментальна функція в Legcord, розроблена для оптимізації швидкості реагування та продуктивності відповідно до ваших потреб. Вплив може відрізнятися залежно від вашого обладнання та використання, тому ми рекомендуємо вам спробувати кожен режим, щоб визначити, який найкраще підходить саме вам.", "settings-prfmMode-desc": "Режим продуктивності це експериментальна функція в Legcord, розроблена для оптимізації швидкості реагування та продуктивності відповідно до ваших потреб. Вплив може відрізнятися залежно від вашого обладнання та використання, тому ми рекомендуємо вам спробувати кожен режим, щоб визначити, який найкраще підходить саме вам.",
"settings-prfmMode-performance": "Продуктивність", "settings-prfmMode-performance": "Продуктивність",
"settings-prfmMode-balanced": "Збалансований",
"settings-prfmMode-battery": "Батарея", "settings-prfmMode-battery": "Батарея",
"settings-prfmMode-memory": "Економія пам'яті",
"settings-prfmMode-voip": "Голос і відео",
"settings-prfmMode-latency": "Низька затримка",
"settings-prfmMode-smoothScreenshare": "Плавна демонстрація екрана",
"settings-prfmMode-dynamic": "Динамічний", "settings-prfmMode-dynamic": "Динамічний",
"settings-prfmMode-vaapi": "VAAPI", "settings-prfmMode-vaapi": "VAAPI",
"settings-disableAutogain": "Вимкнути автоматичне посилення", "settings-disableAutogain": "Вимкнути автоматичне посилення",
@ -142,8 +137,5 @@
"settings-windowsLegacyScanning": "Сканування застарілих версій Windows", "settings-windowsLegacyScanning": "Сканування застарілих версій Windows",
"settings-windowsLegacyScanning-desc": "Використовує застарілий метод для процесів сканування у Windows (до версії 1.1.6). Може покращити сумісність у деяких системах, але менш ефективний.", "settings-windowsLegacyScanning-desc": "Використовує застарілий метод для процесів сканування у Windows (до версії 1.1.6). Може покращити сумісність у деяких системах, але менш ефективний.",
"settings-scanInterval": "Інтервал сканування (мс)", "settings-scanInterval": "Інтервал сканування (мс)",
"settings-scanInterval-desc": "Встановлює частоту (у мілісекундах) сканування процесу. Нижчі значення можуть покращити швидкість виявлення, але можуть збільшити використання процесора.", "settings-scanInterval-desc": "Встановлює частоту (у мілісекундах) сканування процесу. Нижчі значення можуть покращити швидкість виявлення, але можуть збільшити використання процесора."
"settings-startMinimized-off": "Звичайний",
"settings-startMinimized-minimized": "Згорнуто на панель завдань",
"settings-startMinimized-tray": "Сховано в треї"
} }

View file

@ -25,6 +25,6 @@
"settings-transparency-universal": "通用", "settings-transparency-universal": "通用",
"settings-transparency-modern": "现代", "settings-transparency-modern": "现代",
"settings-theme-transparent": "透明", "settings-theme-transparent": "透明",
"settings-transparency-tahoe-warning": "在 macOS 26 Tahoe 上,启用透明效果可能会导致明显卡顿。", "settings-transparency-tahoe-warning": "在 MacOS 26 Tahoe 上,启用透明效果可能会导致明显卡顿。",
"settings-popoutPiP": "通话窗口弹出时保持置顶" "settings-popoutPiP": "通话窗口弹出时保持置顶"
} }

View file

@ -1,9 +1,9 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.5.5/schema.json", "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"includes": ["**", "!**/assets/app/js", "!**/assets/**/*.svg"] "ignore": ["assets/app/js"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
@ -16,11 +16,11 @@
"attributePosition": "auto", "attributePosition": "auto",
"bracketSpacing": true "bracketSpacing": true
}, },
"assist": { "actions": { "source": { "organizeImports": "on" } } }, "organizeImports": { "enabled": true },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"preset": "recommended", "recommended": true,
"complexity": { "complexity": {
"noForEach": "off" "noForEach": "off"
}, },

View file

@ -1,21 +0,0 @@
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"]

View file

@ -1,21 +0,0 @@
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"]

View file

@ -1,13 +0,0 @@
#!/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

View file

@ -1,82 +0,0 @@
# 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.

View file

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

View file

@ -1,9 +0,0 @@
{
"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"
}

View file

@ -1,201 +0,0 @@
/**
* 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 });
};

View file

@ -1,20 +0,0 @@
/**
* 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");
});
};

View file

@ -1,11 +0,0 @@
{
"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"
}

View file

@ -1,29 +0,0 @@
/**
* 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");
});
};

View file

@ -1,18 +0,0 @@
/**
* 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;
};

View file

@ -1,130 +0,0 @@
# 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.

View file

@ -1,33 +1,10 @@
import type { Configuration } from "electron-builder"; 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 = { export const config: Configuration = {
appId: "app.legcord.Legcord", appId: "app.legcord.Legcord",
productName: "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}", artifactName: "Legcord-${version}-${os}-${arch}.${ext}",
beforePack: applyAppImageSandboxFix, beforePack: "./scripts/build/sandboxFix.cjs",
protocols: [ protocols: [
{ {
name: "Discord", name: "Discord",
@ -41,8 +18,6 @@ export const config: Configuration = {
extendInfo: { extendInfo: {
NSMicrophoneUsageDescription: "Legcord requires access to the microphone to function properly.", NSMicrophoneUsageDescription: "Legcord requires access to the microphone to function properly.",
NSCameraUsageDescription: "Legcord requires access to the camera 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, NSCameraUseContinuityCameraDeviceType: true,
"com.apple.security.device.audio-input": true, "com.apple.security.device.audio-input": true,
"com.apple.security.device.camera": true, "com.apple.security.device.camera": true,
@ -62,19 +37,6 @@ 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: { nsis: {
oneClick: false, oneClick: false,
allowToChangeInstallationDirectory: true, allowToChangeInstallationDirectory: true,
@ -102,14 +64,7 @@ export const config: Configuration = {
deb: { deb: {
category: "Network", category: "Network",
icon: "build/icon.icns", icon: "build/icon.icns",
depends: ["libasound2", "libnspr4", "libnss3", "libasound2t64", "libasound2-plugins"], depends: ["libgbm-dev", "libasound2", "libnspr4", "libnss3"],
desktop: {
entry: {
Actions: availableActions,
},
desktopActions: desktopActions("/opt/Legcord/legcord"),
},
fpm: [`${debianLicence()}=/usr/share/doc/legcord/copyright`],
}, },
files: [ files: [
@ -126,20 +81,6 @@ export const config: Configuration = {
electronDownload: { electronDownload: {
cache: ".cache", 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; export default config;

View file

@ -4,7 +4,7 @@
<id>app.legcord.Legcord</id> <id>app.legcord.Legcord</id>
<name>Legcord</name> <name>Legcord</name>
<summary>Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.</summary> <summary>Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.</summary>
<developer id="app.legcord"> <developer>
<name>Legcord Contributors</name> <name>Legcord Contributors</name>
</developer> </developer>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
@ -16,63 +16,19 @@
</description> </description>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<caption>Legcord settings page on macOS</caption> <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> <image type="source">https://github.com/Legcord/Legcord/blob/77e2ccafb221936a99654c237cb385d486780bc7/assets/screenshots/settings.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<caption>Legcord settings open with Shelter configs</caption> <caption>Legcord settings open with Shelter configs</caption>
<image type="source">https://raw.githubusercontent.com/Legcord/Legcord/refs/tags/v1.2.4/assets/screenshots/shelter.png</image> <image type="source">https://github.com/Legcord/Legcord/blob/77e2ccafb221936a99654c237cb385d486780bc7/assets/screenshots/shelter.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<caption>Legcord settings open with custom keybind settings shown</caption> <caption>Legcord settings open with custom keybind settings shown</caption>
<image type="source">https://raw.githubusercontent.com/Legcord/Legcord/refs/tags/v1.2.4/assets/screenshots/keybinds.png</image> <image type="source">https://github.com/Legcord/Legcord/blob/77e2ccafb221936a99654c237cb385d486780bc7/assets/screenshots/keybinds.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <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>Whats 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 &amp; 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"> <release version="1.2.3" date="2026-04-15" type="stable">
<url>https://github.com/Legcord/Legcord/releases/tag/v1.2.3</url> <url>https://github.com/Legcord/Legcord/releases/tag/v1.2.3</url>
<description> <description>
@ -275,6 +231,10 @@
<url>https://github.com/Legcord/Legcord/releases/tag/v1.0.2</url> <url>https://github.com/Legcord/Legcord/releases/tag/v1.0.2</url>
<description/> <description/>
</release> </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"> <release version="v1.0.0" date="2024-10-11" type="stable">
<url>https://github.com/Legcord/Legcord/releases/tag/v1.0.0</url> <url>https://github.com/Legcord/Legcord/releases/tag/v1.0.0</url>
<description/> <description/>
@ -318,4 +278,4 @@
<keyword>Legcord</keyword> <keyword>Legcord</keyword>
<keyword>Equicord</keyword> <keyword>Equicord</keyword>
</keywords> </keywords>
</component> </component>

View file

@ -1,15 +1,15 @@
{ {
"name": "legcord", "name": "legcord",
"version": "1.3.0", "version": "1.2.4",
"description": "Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.", "description": "Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.",
"main": "ts-out/main.js", "main": "ts-out/main.js",
"engines": { "engines": {
"node": ">=26" "node": ">=22"
}, },
"scripts": { "scripts": {
"build:dev": "rollup -c --environment BUILD:dev && node --experimental-strip-types scripts/copyVenmic.ts", "build:dev": "rollup -c --environment BUILD:dev && tsx scripts/copyVenmic.mts",
"build:plugins": "lune ci --repoSubDir src/shelter --to ts-out/plugins", "build:plugins": "lune ci --repoSubDir src/shelter --to ts-out/plugins",
"build": "pnpm build:plugins && rolldown -c rolldown.config.ts && node --experimental-strip-types scripts/copyVenmic.ts", "build": "pnpm build:plugins && rolldown -c rolldown.config.js && tsx scripts/copyVenmic.mts",
"start": "pnpm run build && electron --trace-warnings --ozone-platform-hint=auto ./ts-out/main.js", "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", "startThemeManager": "pnpm run build:dev && electron ./ts-out/main.js themes",
"package": "pnpm run build && electron-builder", "package": "pnpm run build && electron-builder",
@ -18,7 +18,7 @@
"lint:fix": "biome check --write", "lint:fix": "biome check --write",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"CIbuild": "pnpm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip", "CIbuild": "pnpm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip",
"updateMeta": "node --experimental-strip-types scripts/utils/updateMeta.ts" "updateMeta": "tsx scripts/utils/updateMeta.mts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -32,45 +32,39 @@
}, },
"homepage": "https://github.com/Legcord/Legcord#readme", "homepage": "https://github.com/Legcord/Legcord#readme",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.5", "@biomejs/biome": "1.9.4",
"@rolldown-plugin/solid": "^0.1.0", "@rolldown-plugin/solid": "^0.0.4",
"@rollup/plugin-esm-shim": "^0.1.8", "@rollup/plugin-esm-shim": "^0.1.7",
"@types/adm-zip": "^0.5.8", "@types/adm-zip": "^0.5.8",
"@types/node": "^26.1.1", "@types/minimatch": "^6.0.0",
"@types/ws": "^8.18.1", "@types/node": "^22.10.1",
"@uwu/lune": "^1.6.3", "@types/ws": "^8.5.13",
"@uwu/lune": "^1.6.2",
"@uwu/shelter-defs": "^1.5.1", "@uwu/shelter-defs": "^1.5.1",
"@uwu/shelter-ui": "^0.0.6", "babel-preset-solid": "^1.9.3",
"@xmldom/xmldom": "^0.9.10", "electron": "41.2.0",
"app-builder-lib": "^26.15.3", "electron-builder": "26.0.20",
"babel-preset-solid": "^1.9.12", "lucide-solid": "^0.475.0",
"electron": "43.2.0", "rolldown": "1.0.0-rc.15",
"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", "rollup-plugin-copy": "^3.5.0",
"solid-js": "^1.9.14", "solid-js": "^1.9.3",
"solid-motionone": "^1.0.4", "solid-motionone": "^1.0.3",
"spdx-license-list": "^6.11.0", "tsx": "^4.19.2",
"typescript": "^7.0.2", "typescript": "^5.7.2",
"xml-formatter": "^3.7.0" "xml-formatter": "^3.6.6"
}, },
"dependencies": { "dependencies": {
"@jellybrick/dbus-next": "^0.11.1", "adm-zip": "^0.5.16",
"adm-zip": "^0.6.0", "arrpc": "https://github.com/Legcord/arrpc.git#efe7589762470d32b9ba10d529be5acc23cd0e19",
"arrpc": "github:Legcord/arrpc#efe7589762470d32b9ba10d529be5acc23cd0e19", "electron-context-menu": "^4.0.4",
"electron-context-menu": "^4.1.2",
"electron-is-dev": "^3.0.1", "electron-is-dev": "^3.0.1",
"electron-updater": "^6.8.9",
"ms": "^2.1.3",
"spitroast": "^2.1.6",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"ws": "^8.21.1" "electron-updater": "^6.6.2",
"ws": "^8.18.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vencord/venmic": "^7.1.0" "@vencord/venmic": "^6.1.0"
}, },
"packageManager": "pnpm@11.10.0", "packageManager": "pnpm@10.11.0",
"package-manager-strict": false "package-manager-strict": false
} }

4072
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -31,13 +31,9 @@ export default defineConfig([
targets: [ targets: [
{ src: "src/**/**/*.html", dest: "ts-out/html/" }, { src: "src/**/**/*.html", dest: "ts-out/html/" },
{ src: "src/**/**/*.css", dest: "ts-out/css/" }, { 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: "src/**/**/*.js", dest: "ts-out/js/" },
{ src: "package.json", dest: "ts-out/" }, { src: "package.json", dest: "ts-out/" },
{ src: "assets/**/**", dest: "ts-out/assets/" }, { 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" },
], ],
}), }),
], ],

View file

@ -6,21 +6,18 @@
// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js // Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
import fs from "node:fs/promises"; const fs = require("node:fs/promises");
import path from "node:path"; const path = require("node:path");
import AppImageTarget from "app-builder-lib/out/targets/appimage/AppImageTarget.js";
let isApplied = false; let isApplied = false;
export async function applyAppImageSandboxFix() { const hook = async () => {
if (isApplied) return;
isApplied = true;
if (process.platform !== "linux") { if (process.platform !== "linux") {
// this fix is only required on linux // this fix is only required on linux
return; return;
} }
const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget.js");
if (isApplied) return;
isApplied = true;
const oldBuildMethod = AppImageTarget.default.prototype.build; const oldBuildMethod = AppImageTarget.default.prototype.build;
AppImageTarget.default.prototype.build = async function (...args) { AppImageTarget.default.prototype.build = async function (...args) {
console.log("Running AppImage builder hook", args); console.log("Running AppImage builder hook", args);
@ -72,4 +69,6 @@ exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ]
return ret; return ret;
}; };
} };
module.exports = hook;

View file

@ -1,66 +0,0 @@
// 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;
}

View file

@ -50,17 +50,6 @@ const latestReleaseInformation = await fetch("https://api.github.com/repos/Legco
}, },
}).then((res) => res.json()); }).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 metaInfo = await fs.readFile("./meta/app.legcord.Legcord.metainfo.xml", "utf-8");
const parser = new DOMParser().parseFromString(metaInfo, "text/xml"); const parser = new DOMParser().parseFromString(metaInfo, "text/xml");

View file

@ -6,7 +6,6 @@ export type KeybindActions =
| "navigateForward" | "navigateForward"
| "openQuickCss" | "openQuickCss"
| "pushToTalk" | "pushToTalk"
| "openSettings"
| "runJavascript"; | "runJavascript";
export interface Keybind { export interface Keybind {
accelerator: Electron.Accelerator; accelerator: Electron.Accelerator;

View file

@ -5,21 +5,6 @@ import type { Keybind } from "./keybind.js";
import type { Settings } from "./settings.js"; import type { Settings } from "./settings.js";
import type { ThemeManifest } from "./themeManifest.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 { export interface LegcordWindow {
window: { window: {
show: () => void; show: () => void;
@ -46,8 +31,6 @@ export interface LegcordWindow {
openCustomIconDialog: () => void; openCustomIconDialog: () => void;
copyDebugInfo: () => void; copyDebugInfo: () => void;
copyGPUInfo: () => void; copyGPUInfo: () => void;
openWebRTCInternals: () => void;
openGPUInfo: () => void;
setLang(lang: string): () => void; setLang(lang: string): () => void;
addKeybind: (keybind: Keybind) => void; addKeybind: (keybind: Keybind) => void;
toggleKeybind: (id: string) => void; toggleKeybind: (id: string) => void;
@ -80,7 +63,6 @@ export interface LegcordWindow {
uninstall: (id: string) => void; uninstall: (id: string) => void;
set: (id: string, state: boolean) => void; set: (id: string, state: boolean) => void;
getThemes: () => Readonly<ThemeManifest[]>; getThemes: () => Readonly<ThemeManifest[]>;
refresh: () => Readonly<ThemeManifest[]>;
openQuickCss: () => void; openQuickCss: () => void;
edit: (id: string) => void; edit: (id: string) => void;
folder: (id: string) => void; folder: (id: string) => void;
@ -109,12 +91,6 @@ export interface LegcordWindow {
save(data: string): Promise<{ ok: true } | { ok: false; error: string }>; save(data: string): Promise<{ ok: true } | { ok: false; error: string }>;
restore(): Promise<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. */ /** Plugin storage API. Requires user to enable "Extended plugin abilities" in Legcord settings. */
fs: { fs: {
writeFile: ( writeFile: (

View file

@ -25,9 +25,6 @@ export interface AudioSettings {
loopbackType: "loopback" | "loopbackWithMute"; loopbackType: "loopback" | "loopbackWithMute";
} }
/** Chromium/Electron proxy modes — mirrors browser proxy settings. */
export type ProxyMode = "system" | "direct" | "fixed_servers" | "pac_script" | "auto_detect";
export interface Settings { export interface Settings {
// Referenced for detecting a broken config. // Referenced for detecting a broken config.
"0"?: string; "0"?: string;
@ -46,24 +43,12 @@ export interface Settings {
mods: ValidMods[]; mods: ValidMods[];
mobileMode: boolean; mobileMode: boolean;
skipSplash: boolean; skipSplash: boolean;
performanceMode: performanceMode: "battery" | "dynamic" | "performance" | "smoothScreenshare" | "none";
| "battery"
| "dynamic"
| "performance"
| "balanced"
| "memory"
| "voip"
| "latency"
| "smoothScreenshare"
| "none";
customJsBundle: RequestInfo | URL | string; customJsBundle: RequestInfo | URL | string;
customCssBundle: RequestInfo | URL | string; customCssBundle: RequestInfo | URL | string;
/** How the main window appears on launch: normal, taskbar minimized, or tray-only. */ startMinimized: boolean;
startMinimized: "off" | "minimized" | "tray";
keybinds: Keybind[]; keybinds: Keybind[];
hardwareAcceleration: boolean; hardwareAcceleration: boolean;
/** Rewrite H.264 Constrained Baseline (42e0) → Baseline (4200) in WebRTC SDP for HW encode. */
sdpH264BaselineRewrite: boolean;
useMacSystemPicker: boolean; useMacSystemPicker: boolean;
inviteWebsocket: boolean; inviteWebsocket: boolean;
disableAutogain: boolean; disableAutogain: boolean;
@ -82,14 +67,6 @@ export interface Settings {
quickCss: boolean; quickCss: boolean;
autoScroll: boolean; autoScroll: boolean;
additionalArguments: string; additionalArguments: string;
/** How Legcord resolves HTTP(S) proxies (Chromium + main-process fetch). */
proxyMode: ProxyMode;
/** Fixed proxy rules, e.g. `http://127.0.0.1:8080` or `socks5://host:1080`. */
proxyRules: string;
/** Hosts that bypass the proxy (comma-separated), e.g. `<local>,*.intranet.example`. */
proxyBypassRules: string;
/** PAC script URL when proxyMode is `pac_script`. */
proxyPacScript: string;
noBundleUpdates: ValidMods[]; noBundleUpdates: ValidMods[];
automaticUpdates: boolean; automaticUpdates: boolean;
overlayButtonColor: string; overlayButtonColor: string;
@ -99,7 +76,4 @@ export interface Settings {
modCache?: Record<ValidMods, string>; modCache?: Record<ValidMods, string>;
extendedPluginAbilities: boolean; extendedPluginAbilities: boolean;
supportBannerDismissed: boolean; supportBannerDismissed: boolean;
pluginStates?: Record<string, boolean>;
// Remove below once the plugin system is fully implemented.
showExperimentalPluginMenu: boolean;
} }

View file

@ -4,6 +4,4 @@ export interface WindowState {
x: number; x: number;
y: number; y: number;
isMaximized: boolean; isMaximized: boolean;
displayId?: number;
displayScaleFactor?: number;
} }

View file

@ -1,4 +1,4 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { app } from "electron"; import { app } from "electron";
@ -38,7 +38,6 @@ export interface BackupManifest extends BackupSavePayload {
export interface BackupPaths { export interface BackupPaths {
userDataPath: string; userDataPath: string;
themesPath: string; themesPath: string;
extensionsPath: string;
pluginsPath: string; pluginsPath: string;
pluginStoragePath: string; pluginStoragePath: string;
quickCssPath: string; quickCssPath: string;
@ -115,8 +114,7 @@ export function buildBackupZipBuffer(payload: BackupSavePayload, paths: BackupPa
entries.push(...walkFiles(paths.themesPath, "data/themes")); entries.push(...walkFiles(paths.themesPath, "data/themes"));
} }
if (includes.legcordExtensionPlugins) { if (includes.legcordExtensionPlugins) {
entries.push(...walkFiles(paths.extensionsPath, "data/plugins")); entries.push(...walkFiles(paths.pluginsPath, "data/plugins"));
entries.push(...walkFiles(paths.pluginsPath, "data/runtime-plugins"));
entries.push(...walkFiles(paths.pluginStoragePath, "data/plugin-storage")); entries.push(...walkFiles(paths.pluginStoragePath, "data/plugin-storage"));
} }
if (includes.modBundles) { if (includes.modBundles) {
@ -199,21 +197,9 @@ export function applyBackupFromMap(
if (name.startsWith("data/plugins/")) { if (name.startsWith("data/plugins/")) {
if (!inc.legcordExtensionPlugins) continue; if (!inc.legcordExtensionPlugins) continue;
const rest = name.slice("data/plugins/".length); const rest = name.slice("data/plugins/".length);
const dest = resolvePathUnderBaseDir(paths.extensionsPath, rest);
if (!dest) {
console.warn(`[backup] Skipping unsafe zip path (plugins): ${name}`);
continue;
}
writeFileEnsuringDirs(dest, data);
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); const dest = resolvePathUnderBaseDir(paths.pluginsPath, rest);
if (!dest) { if (!dest) {
console.warn(`[backup] Skipping unsafe zip path (runtime-plugins): ${name}`); console.warn(`[backup] Skipping unsafe zip path (plugins): ${name}`);
continue; continue;
} }
writeFileEnsuringDirs(dest, data); writeFileEnsuringDirs(dest, data);

View file

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

View file

@ -10,9 +10,9 @@ export let firstRun: boolean;
// Performance optimization: Cache config to avoid reading file on every call // Performance optimization: Cache config to avoid reading file on every call
let configCache: Settings | null = null; let configCache: Settings | null = null;
let configCacheTime = 0; let configCacheTime = 0;
const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds const CONFIG_CACHE_TTL = 1000; // Cache for 1 second
const defaults: Settings = { const defaults: Settings = {
windowStyle: "overlay", windowStyle: "default",
channel: "stable", channel: "stable",
bounceOnPing: false, bounceOnPing: false,
csp: "none", csp: "none",
@ -34,23 +34,22 @@ const defaults: Settings = {
loopbackType: "loopback", loopbackType: "loopback",
}, },
multiInstance: false, multiInstance: false,
mods: [], mods: ["vencord"],
transparency: "none", transparency: "none",
windowMaterial: "mica", windowMaterial: "mica",
spellcheck: true, spellcheck: true,
hardwareAcceleration: true, hardwareAcceleration: true,
sdpH264BaselineRewrite: true,
performanceMode: "none", performanceMode: "none",
skipSplash: false, skipSplash: false,
inviteWebsocket: true, inviteWebsocket: true,
startMinimized: "off", startMinimized: false,
disableHttpCache: false, disableHttpCache: false,
customJsBundle: "https://legcord.app/placeholder.js", customJsBundle: "https://legcord.app/placeholder.js",
customCssBundle: "https://legcord.app/placeholder.css", customCssBundle: "https://legcord.app/placeholder.css",
disableAutogain: false, disableAutogain: false,
autoHideMenuBar: true, autoHideMenuBar: true,
blockPowerSavingInVoiceChat: false, blockPowerSavingInVoiceChat: false,
useMacSystemPicker: false, useMacSystemPicker: true,
mobileMode: false, mobileMode: false,
tray: "dynamic", tray: "dynamic",
doneSetup: false, doneSetup: false,
@ -61,10 +60,6 @@ const defaults: Settings = {
noBundleUpdates: [], noBundleUpdates: [],
automaticUpdates: false, automaticUpdates: false,
additionalArguments: "", additionalArguments: "",
proxyMode: "system",
proxyRules: "",
proxyBypassRules: "<local>",
proxyPacScript: "",
customIcon: join(import.meta.dirname, "../", "/assets/desktop.png"), customIcon: join(import.meta.dirname, "../", "/assets/desktop.png"),
smoothScroll: true, smoothScroll: true,
autoScroll: false, autoScroll: false,
@ -72,8 +67,6 @@ const defaults: Settings = {
extendedPluginAbilities: false, extendedPluginAbilities: false,
quickCss: true, quickCss: true,
supportBannerDismissed: false, supportBannerDismissed: false,
showExperimentalPluginMenu: false,
pluginStates: {},
}; };
const safeMode: Settings = { const safeMode: Settings = {
@ -82,12 +75,10 @@ const safeMode: Settings = {
windowStyle: "native", windowStyle: "native",
csp: "vanilla", csp: "vanilla",
hardwareAcceleration: false, hardwareAcceleration: false,
sdpH264BaselineRewrite: false,
disableHttpCache: true, disableHttpCache: true,
vaapi: false, vaapi: false,
additionalArguments: "", additionalArguments: "",
extendedPluginAbilities: false, extendedPluginAbilities: false,
showExperimentalPluginMenu: false,
quickCss: false, quickCss: false,
}; };
@ -122,31 +113,6 @@ export function getConfig<K extends keyof Settings>(object: K): Settings[K] {
configCacheTime = now; configCacheTime = now;
return returnData[object]; 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 { export function setConfig<K extends keyof Settings>(object: K, toSet: Settings[K]): void {
const rawData = readFileSync(getConfigLocation(), "utf-8"); const rawData = readFileSync(getConfigLocation(), "utf-8");
const parsed = JSON.parse(rawData) as Settings; const parsed = JSON.parse(rawData) as Settings;
@ -209,15 +175,10 @@ export function checkIfConfigExists(): void {
export function checkIfConfigIsBroken(): void { export function checkIfConfigIsBroken(): void {
try { try {
const settingsData = readFileSync(getConfigLocation(), "utf-8"); const settingsData = readFileSync(getConfigLocation(), "utf-8");
const settingsObject = JSON.parse(settingsData) as Settings & Record<string, unknown>; const settingsObject = JSON.parse(settingsData) as Settings;
// 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 // Performance optimization: Update cache after validation
configCache = settingsObject as Settings; configCache = settingsObject;
configCacheTime = Date.now(); configCacheTime = Date.now();
let configWasFine = true; let configWasFine = true;

View file

@ -1,7 +1,8 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import type { Game, GameList } from "arrpc";
import { app } from "electron"; import { app } from "electron";
import type { Game, GameList } from "arrpc";
export function getDetectablesPath() { export function getDetectablesPath() {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");

View file

@ -1,12 +1,7 @@
import type { BrowserWindow } from "electron"; import type { BrowserWindow } from "electron";
let scriptCounter = 0;
export function addStyle(styleUrl: string): void { 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"); const style = document.createElement("link");
style.id = id;
style.rel = "stylesheet"; style.rel = "stylesheet";
style.type = "text/css"; style.type = "text/css";
style.href = styleUrl; style.href = styleUrl;
@ -14,7 +9,6 @@ export function addStyle(styleUrl: string): void {
} }
export function addTheme(id: string, styleString: string): void { export function addTheme(id: string, styleString: string): void {
if (document.getElementById(id)) return;
const style = document.createElement("style"); const style = document.createElement("style");
style.textContent = styleString; style.textContent = styleString;
style.id = id; style.id = id;
@ -22,33 +16,27 @@ export function addTheme(id: string, styleString: string): void {
} }
export function addScript(scriptString: string): void { export function addScript(scriptString: string): void {
const id = `legcord-script-${++scriptCounter}`;
if (document.getElementById(id)) return;
const script = document.createElement("script"); const script = document.createElement("script");
script.id = id;
script.appendChild(document.createTextNode(scriptString)); script.appendChild(document.createTextNode(scriptString));
document.body.append(script); document.body.append(script);
} }
export async function injectJS(inject: string): Promise<void> { 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 js = await (await fetch(`${inject}`)).text();
const el = document.createElement("script"); const el = document.createElement("script");
el.id = id;
el.appendChild(document.createTextNode(js)); el.appendChild(document.createTextNode(js));
document.body.appendChild(el); document.body.appendChild(el);
} }
export function navigateTo(passedWindow: BrowserWindow, url: string): void { export function navigateTo(passedWindow: BrowserWindow, url: string): void {
// Sanitize: only allow path-like URLs (no protocol, no quotes) console.log(`[legcord deeplink] Navigating to ${url}`);
const sanitized = url.replace(/[^a-zA-Z0-9/_\-@.]/g, ""); const safeUrl = JSON.stringify(url);
console.log(`[legcord deeplink] Navigating to ${sanitized}`); passedWindow.webContents.executeJavaScript(`
passedWindow.webContents.executeJavaScript( history.pushState({}, null, ${safeUrl});
`history.pushState({}, null, ${JSON.stringify(sanitized)});window.dispatchEvent(new PopStateEvent("popstate", {}));`, window.dispatchEvent(new PopStateEvent("popstate", {}));
); `);
passedWindow.setSkipTaskbar(false);
if (passedWindow.isMinimized()) passedWindow.restore();
passedWindow.show();
passedWindow.focus(); passedWindow.focus();
} }

View file

@ -1,4 +1,4 @@
import { existsSync, readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { app, powerMonitor } from "electron"; import { app, powerMonitor } from "electron";
import isDev from "electron-is-dev"; import isDev from "electron-is-dev";
@ -32,88 +32,16 @@ const performance: Preset = {
disableFeatures: ["Vulkan"], disableFeatures: ["Vulkan"],
}; };
/** Light GPU boost without forcing the discrete GPU — a middle ground. */
const balanced: Preset = {
switches: [["enable-gpu-rasterization"], ["enable-zero-copy"], ["ignore-gpu-blocklist"]],
enableFeatures: [
"CanvasOopRasterization",
"UseSkiaRenderer",
"WebAssemblyLazyCompilation",
"CalculateNativeWinOcclusion",
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
],
disableFeatures: ["Vulkan"],
};
/** Reduce RAM / CPU usage at the cost of some visual smoothness. */
const memory: Preset = {
switches: [
["enable-low-end-device-mode"],
["enable-low-res-tiling"],
["process-per-site"],
["renderer-process-limit", "2"],
["force_low_power_gpu"],
["disk-cache-size", "67108864"], // 64 MB
["skia-resource-cache-limit-mb", "64"],
],
enableFeatures: ["CalculateNativeWinOcclusion", "TurnOffStreamingMediaCachingOnBattery"],
disableFeatures: [],
};
/** Favor voice/video call quality. 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 = { const smoothExperiment: Preset = {
switches: [ switches: [
["enable-gpu-rasterization"], ["enable-gpu-rasterization"],
["enable-zero-copy"], ["enable-zero-copy"],
["ignore-gpu-blocklist"], ["ignore-gpu-blocklist"],
["enable-accelerated-video-decode"],
["disable-background-timer-throttling"], ["disable-background-timer-throttling"],
["disable-renderer-backgrounding"], ["disable-renderer-backgrounding"],
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"], ["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
["force_high_performance_gpu"], ["force_high_performance_gpu"],
// Do NOT set use-gl=desktop here. On Electron 43+/macOS, Chromium only allows ["use-gl", "desktop"],
// ANGLE (metal/opengl); use-gl=desktop fails GPU init and then disables all
// HW acceleration (including VideoToolbox encode) after repeated crashes.
], ],
enableFeatures: [ enableFeatures: [
"EnableDrDc", "EnableDrDc",
@ -122,11 +50,10 @@ const smoothExperiment: Preset = {
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes", "ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
"UseSkiaRenderer", "UseSkiaRenderer",
"WebAssemblyLazyCompilation", "WebAssemblyLazyCompilation",
"WebRtcHWEncoding", "AcceleratedVideoDecodeLinuxGL",
"WebRtcHWDecoding",
"AcceleratedVideoEncoder", "AcceleratedVideoEncoder",
"AcceleratedVideoDecoder", "AcceleratedVideoDecoder",
"ZeroCopyDesktopCapture", "AcceleratedVideoDecodeLinuxZeroCopyGL",
], ],
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"], disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
}; };
@ -139,178 +66,27 @@ const battery: Preset = {
["enable-low-res-tiling"], ["enable-low-res-tiling"],
["process-per-site"], ["process-per-site"],
], ],
enableFeatures: ["TurnOffStreamingMediaCachingOnBattery", "CalculateNativeWinOcclusion"], enableFeatures: ["TurnOffStreamingMediaCachingOnBattery"],
disableFeatures: [], 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: [ switches: [
["ignore-gpu-blocklist"], ["ignore-gpu-blocklist"],
["enable-zero-copy"],
["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-gpu-rasterization"],
["enable-zero-copy"], ["enable-zero-copy"],
["enable-accelerated-video-decode"],
["force_high_performance_gpu"], ["force_high_performance_gpu"],
// ANGLE+OpenGL required for AcceleratedVideoDecodeLinuxGL on Wayland. ["use-gl", "desktop"],
// 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: [ enableFeatures: [
"VaapiIgnoreDriverChecks", "AcceleratedVideoDecodeLinuxGL",
"VaapiVideoDecoder",
"VaapiVideoEncoder",
"AcceleratedVideoEncoder", "AcceleratedVideoEncoder",
"AcceleratedVideoDecoder", "AcceleratedVideoDecoder",
"AcceleratedVideoDecodeLinuxGL",
"AcceleratedVideoDecodeLinuxZeroCopyGL", "AcceleratedVideoDecodeLinuxZeroCopyGL",
"WebRtcHWEncoding",
"WebRtcHWDecoding",
"ZeroCopyDesktopCapture",
"CanvasOopRasterization",
], ],
// Vulkan is incompatible with ozone wayland and breaks VAAPI GL interop. disableFeatures: ["UseChromeOSDirectVideoDecoder"],
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) * Load custom flags from JSON file in user data directory (cached after first load)
* Path: * Path:
@ -384,14 +160,6 @@ function mergeWithCustomFlags(preset: Preset): Preset {
}; };
} }
function mergePresets(base: Preset, extra: Preset): Preset {
return {
switches: [...base.switches, ...extra.switches],
enableFeatures: [...base.enableFeatures, ...extra.enableFeatures],
disableFeatures: [...base.disableFeatures, ...extra.disableFeatures],
};
}
export function getPreset(): Preset | undefined { export function getPreset(): Preset | undefined {
// MIT License // MIT License
@ -414,58 +182,31 @@ export function getPreset(): Preset | undefined {
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
let preset: Preset | undefined; if (getConfig("vaapi")) {
console.log("VAAPI mode enabled");
mergeWithCustomFlags(vaapi);
}
switch (getConfig("performanceMode")) { switch (getConfig("performanceMode")) {
case "dynamic": case "dynamic":
if (powerMonitor.isOnBatteryPower()) { if (powerMonitor.isOnBatteryPower()) {
console.log("Battery mode enabled"); console.log("Battery mode enabled");
preset = battery; return mergeWithCustomFlags(battery);
} else { } else {
console.log("Performance mode enabled"); console.log("Performance mode enabled");
preset = performance; return mergeWithCustomFlags(performance);
} }
break;
case "performance": case "performance":
console.log("Performance mode enabled"); console.log("Performance mode enabled");
preset = performance; return mergeWithCustomFlags(performance);
break;
case "balanced":
console.log("Balanced mode enabled");
preset = balanced;
break;
case "battery": case "battery":
console.log("Battery mode enabled"); console.log("Battery mode enabled");
preset = battery; return mergeWithCustomFlags(battery);
break;
case "memory":
console.log("Memory saver mode enabled");
preset = memory;
break;
case "voip":
console.log("Voice & video mode enabled");
preset = voip;
break;
case "latency":
console.log("Low latency mode enabled");
preset = latency;
break;
case "smoothScreenshare": case "smoothScreenshare":
console.log("Smooth screenshare mode enabled"); console.log("Smooth screenshare mode enabled");
preset = smoothExperiment; return mergeWithCustomFlags(smoothExperiment);
break;
default: default:
console.log("No performance modes set"); 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);
}
} }
/** /**

View file

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

View file

@ -2,8 +2,6 @@ import path from "node:path";
import { app, shell } from "electron"; import { app, shell } from "electron";
import type { Keybind } from "../@types/keybind.js"; import type { Keybind } from "../@types/keybind.js";
import { mainWindows } from "../discord/window.js"; import { mainWindows } from "../discord/window.js";
import { navigateTo } from "./dom.js";
let isAudioEngineEnabled = false; let isAudioEngineEnabled = false;
export function runAction(keybind: Keybind) { export function runAction(keybind: Keybind) {
@ -29,9 +27,6 @@ export function runAction(keybind: Keybind) {
case "openQuickCss": case "openQuickCss":
openQuickCss(); openQuickCss();
break; break;
case "openSettings":
openSettings();
break;
case "runJavascript": case "runJavascript":
if (!keybind.js) break; if (!keybind.js) break;
runJavascript(keybind.js); runJavascript(keybind.js);
@ -127,10 +122,3 @@ function runJavascript(js: string) {
window.webContents.executeJavaScript(js); 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");
});
}

View file

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

View file

@ -1,88 +0,0 @@
/** 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);
}

View file

@ -1,6 +1,6 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { app, type BrowserWindow } from "electron"; import { type BrowserWindow, app } from "electron";
import type { ThemeManifest } from "../@types/themeManifest.js"; import type { ThemeManifest } from "../@types/themeManifest.js";
import { mainWindows } from "../discord/window.js"; import { mainWindows } from "../discord/window.js";
import { getConfig } from "./config.js"; import { getConfig } from "./config.js";
@ -112,111 +112,6 @@ 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 { export function injectThemesMain(browserWindow: BrowserWindow): void {
if (process.argv.includes("--safe-mode")) return; if (process.argv.includes("--safe-mode")) return;
if (!fs.existsSync(themesFolder)) { if (!fs.existsSync(themesFolder)) {
@ -228,13 +123,11 @@ export function injectThemesMain(browserWindow: BrowserWindow): void {
const files = fs.readdirSync(themesFolder); const files = fs.readdirSync(themesFolder);
for (const file of files) { for (const file of files) {
const themePath = path.join(themesFolder, file); const themePath = path.join(themesFolder, file);
if (fs.statSync(themePath).isFile() && (file.endsWith(".css") || file.endsWith(".theme.css"))) { if (fs.statSync(themePath).isFile() && file.endsWith(".DS_Store")) {
console.log(`[Theme Manager] Local theme detected: ${themePath}`); console.log(`[Theme Manager] Local theme detected: ${themePath}`);
const code = fs.readFileSync(themePath, "utf8"); installTheme(themePath).then(() => {
installThemeFromCode(code);
try {
fs.unlinkSync(themePath); fs.unlinkSync(themePath);
} catch {} });
} else { } else {
try { try {
const themeFile = getThemeManifest(file); const themeFile = getThemeManifest(file);
@ -275,11 +168,10 @@ export function uninstallTheme(id: string) {
fs.rmdirSync(path.join(themesFolder, `${id}-BD`), { recursive: true }); fs.rmdirSync(path.join(themesFolder, `${id}-BD`), { recursive: true });
console.log(`Removed ${id} folder`); console.log(`Removed ${id} folder`);
} }
themeManifestCache.delete(id);
invalidateThemeListCache();
} }
export function setThemeEnabled(id: string, enabled: boolean) { export function setThemeEnabled(id: string, enabled: boolean) {
// Performance optimization: Use cached manifest if available
let manifest = getThemeManifest(id); let manifest = getThemeManifest(id);
if (!manifest) { if (!manifest) {
manifest = JSON.parse(fs.readFileSync(path.join(themesFolder, id, "/manifest.json"), "utf8")) as ThemeManifest; manifest = JSON.parse(fs.readFileSync(path.join(themesFolder, id, "/manifest.json"), "utf8")) as ThemeManifest;
@ -298,31 +190,13 @@ export function setThemeEnabled(id: string, enabled: boolean) {
passedWindow.webContents.send("removeTheme", id); passedWindow.webContents.send("removeTheme", id);
console.log(`[Theme Manager] Removing ${manifest.name} made by ${manifest.author}`); console.log(`[Theme Manager] Removing ${manifest.name} made by ${manifest.author}`);
} }
return true;
}); });
} }
manifest.enabled = enabled; manifest.enabled = enabled;
fs.writeFileSync(`${themesFolder}/${id}/manifest.json`, JSON.stringify(manifest)); fs.writeFileSync(`${themesFolder}/${id}/manifest.json`, JSON.stringify(manifest));
// Performance optimization: Invalidate cache
themeManifestCache.delete(id); 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) { export async function installTheme(linkOrPath: string) {
@ -334,7 +208,19 @@ export async function installTheme(linkOrPath: string) {
} else { } else {
code = fs.readFileSync(linkOrPath, "utf8"); code = fs.readFileSync(linkOrPath, "utf8");
} }
installThemeFromCode(code, isLinkImport ? linkOrPath : undefined); 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);
} }
export function initQuickCss(browserWindow: BrowserWindow) { export function initQuickCss(browserWindow: BrowserWindow) {

View file

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

View file

@ -6,7 +6,7 @@ import type { WindowState } from "../@types/windowState.js";
// Performance optimization: Cache window state to avoid reading file on every call // Performance optimization: Cache window state to avoid reading file on every call
let windowStateCache: WindowState | null = null; let windowStateCache: WindowState | null = null;
let windowStateCacheTime = 0; let windowStateCacheTime = 0;
const WINDOW_STATE_CACHE_TTL = 5000; // Cache for 5 seconds const WINDOW_STATE_CACHE_TTL = 1000; // Cache for 1 second
export function getWindowStateLocation() { export function getWindowStateLocation() {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");

View file

@ -1,37 +0,0 @@
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;
}
}

View file

@ -1,37 +1,36 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<title>Legcord Quick CSS Editor</title> <title>Legcord Quick CSS Editor</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<link rel="stylesheet" href="legcord://assets/css/editor.css"> <link rel="stylesheet" href="legcord://assets/css/editor.css">
</head> </head>
<body> <body>
<script type="text/javascript" src="./monaco/vs/loader.js"></script> <script type="text/javascript" src="legcord://assets/js/monacoLoader.js"></script>
<div id="editorCode"></div> <div id="editorCode"></div>
<script> <script>
const cssCode = cssEditor.get; 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({ require.config({
paths: { vs } paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor/min/vs" }
}); });
window.MonacoEnvironment = { window.MonacoEnvironment = {
getWorkerUrl: () => getWorkerUrl: function(workerId, label) {
`data:text/javascript;charset=utf-8,${encodeURIComponent(` return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoRoot)} }; self.MonacoEnvironment = {
importScripts(${JSON.stringify(`${vs}/base/worker/workerMain.js`)}); baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/'
`)}`, };
importScripts('https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/base/worker/workerMain.js');`)}`;
}
}; };
// Monaco init // Monaco init
require(["vs/editor/editor.main"], () => { require(["vs/editor/editor.main"], function() {
createEditor(editorCode); createEditor(editorCode);
}); });
function createEditor(editorContainer) { function createEditor(editorContainer) {
const editor = monaco.editor.create(editorContainer, { let editor = monaco.editor.create(editorContainer, {
value: cssCode, value: cssCode,
language: "css", language: "css",
minimap: { enabled: false }, minimap: { enabled: false },
@ -52,6 +51,8 @@
cssEditor.set(editor.getValue()); cssEditor.set(editor.getValue());
}); });
} }
</script> </script>
</body> </body>
</html> </html>

View file

@ -14,7 +14,7 @@ export function openCssEditor(file: string) {
preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"), preload: path.join(import.meta.dirname, "cssEditor", "preload.mjs"),
}, },
}); });
void cssWindow.loadFile(path.join(import.meta.dirname, "html", "editor.html")); cssWindow.loadURL(`file://${import.meta.dirname}/html/editor.html`);
ipcMain.on("editor-setCSS", (_event, css: string) => { ipcMain.on("editor-setCSS", (_event, css: string) => {
fs.writeFileSync(file, css); fs.writeFileSync(file, css);

View file

@ -1,186 +0,0 @@
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;
}

View file

@ -54,7 +54,8 @@ async function cacheCheck(mod: ValidMods) {
} }
try { try {
const latestRef = await getRef(modData[mod].repoData); const latestRef = await getRef(modData[mod].repoData);
if (latestRef === modCache![mod]) { // biome-ignore lint/correctness/noConstantCondition: https://github.com/Legcord/Legcord/issues/763
if (/*latestRef === modCache![mod]*/ false) {
console.log(`[Mod Loader]: ${mod} Cache hit!`); console.log(`[Mod Loader]: ${mod} Cache hit!`);
return; return;
} else { } else {

View file

@ -1,25 +1,24 @@
import { existsSync, mkdirSync, readdirSync } from "node:fs"; import { existsSync, mkdirSync, readdirSync } from "node:fs";
import { platform } from "node:os"; import { platform } from "node:os";
import { app, session } from "electron"; import { app, session } from "electron";
const pluginFolder = `${app.getPath("userData")}/plugins`;
const extensionFolder = `${app.getPath("userData")}/extensions`;
let prefix = ""; let prefix = "";
if (!existsSync(extensionFolder)) { if (!existsSync(pluginFolder)) {
mkdirSync(extensionFolder); mkdirSync(pluginFolder);
console.log("Created missing extensions folder"); console.log("Created missing plugin folder");
} }
await app.whenReady().then(() => { await app.whenReady().then(() => {
readdirSync(extensionFolder).forEach(async (file) => { readdirSync(pluginFolder).forEach(async (file) => {
try { try {
// NOTE - The below type assertion is just what we need from the chrome manifest // NOTE - The below type assertion is just what we need from the chrome manifest
if (platform() === "win32") prefix = "file://"; if (platform() === "win32") prefix = "file://";
const manifest = (await import(`${prefix}${extensionFolder}/${file}/manifest.json`, { const manifest = (await import(`${prefix}${pluginFolder}/${file}/manifest.json`, {
with: { type: "json" }, with: { type: "json" },
})) as { name: string; author: string; type: "json" }; })) as { name: string; author: string; type: "json" };
void session.defaultSession.loadExtension(`${extensionFolder}/${file}`); // NOTE - Awaiting this will cause plugins to not inject void session.defaultSession.loadExtension(`${pluginFolder}/${file}`); // NOTE - Awaiting this will cause plugins to not inject
console.log(`[Mod loader] Loaded ${manifest.name} made by ${manifest.author}`); console.log(`[Mod loader] Loaded ${manifest.name} made by ${manifest.author}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View file

@ -19,7 +19,7 @@ export function registerGlobalKeybinds() {
app.on("will-quit", () => { app.on("will-quit", () => {
try { try {
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
} catch (_e) {} } catch (e) {}
}); });
export function refreshGlobalKeybinds() { export function refreshGlobalKeybinds() {

View file

@ -1,15 +1,15 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { Game } from "arrpc"; import type { Game } from "arrpc";
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from "electron"; import { type BrowserWindow, app, clipboard, dialog, ipcMain, shell } from "electron";
import isDev from "electron-is-dev"; import isDev from "electron-is-dev";
import type { Keybind } from "../@types/keybind.js"; import type { Keybind } from "../@types/keybind.js";
import type { Settings } from "../@types/settings.js"; import type { Settings } from "../@types/settings.js";
import type { ThemeManifest } from "../@types/themeManifest.js"; import type { ThemeManifest } from "../@types/themeManifest.js";
import { import {
applyBackupFromMap,
type BackupSavePayload, type BackupSavePayload,
applyBackupFromMap,
buildBackupZipBuffer, buildBackupZipBuffer,
readBackupZipToMap, readBackupZipToMap,
} from "../common/backup.js"; } from "../common/backup.js";
@ -21,32 +21,20 @@ import {
import { getConfig, getConfigLocation, setConfig, setConfigBulk } from "../common/config.js"; import { getConfig, getConfigLocation, setConfig, setConfigBulk } from "../common/config.js";
import { addDetectable, getDetectables, removeDetectable } from "../common/detectables.js"; import { addDetectable, getDetectables, removeDetectable } from "../common/detectables.js";
import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js"; import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js";
import { import { disableQuickCss, initQuickCss, installTheme, setThemeEnabled, uninstallTheme } from "../common/themes.js";
disableQuickCss,
getCachedThemeList,
initQuickCss,
installTheme,
invalidateThemeListCache,
setThemeEnabled,
startThemeWatcher,
uninstallTheme,
} from "../common/themes.js";
import { getDisplayVersion, getVersion } from "../common/version.js"; import { getDisplayVersion, getVersion } from "../common/version.js";
import { applyStartupWindowVisibility, revealWindow } from "../common/windowVisibility.js";
import { openCssEditor } from "../cssEditor/main.js"; import { openCssEditor } from "../cssEditor/main.js";
import { getAppliedFlags, handleRestart } from "../main.js"; import { getAppliedFlags, handleRestart } from "../main.js";
import { isPowerSavingEnabled, setPowerSaving } from "../power.js"; import { isPowerSavingEnabled, setPowerSaving } from "../power.js";
import constPaths from "../shared/consts/paths.js"; import constPaths from "../shared/consts/paths.js";
import { splashWindow } from "../splash/main.js"; import { splashWindow } from "../splash/main.js";
import { refreshGlobalKeybinds } from "./globalKeybinds.js"; import { refreshGlobalKeybinds } from "./globalKeybinds.js";
import { getRuntimeEntries, getRuntimeScript, listPlugins, reloadPlugin, setPluginEnabled } from "./plugins/manager.js";
import { processList, refreshProcessList } from "./rpcProcess.js"; import { processList, refreshProcessList } from "./rpcProcess.js";
import { importGuilds, mainTouchBar, setVoiceState, voiceTouchBar } from "./touchbar.js"; import { importGuilds, mainTouchBar, setVoiceState, voiceTouchBar } from "./touchbar.js";
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storagePath = path.join(userDataPath, "/storage/"); const storagePath = path.join(userDataPath, "/storage/");
const themesPath = path.join(userDataPath, "/themes/"); const themesPath = path.join(userDataPath, "/themes/");
const extensionsPath = path.join(userDataPath, "/extensions/");
const pluginsPath = path.join(userDataPath, "/plugins/"); const pluginsPath = path.join(userDataPath, "/plugins/");
const pluginStoragePath = path.join(userDataPath, "/plugin-storage/"); const pluginStoragePath = path.join(userDataPath, "/plugin-storage/");
const quickCssPath = path.join(userDataPath, "/quickCss.css"); const quickCssPath = path.join(userDataPath, "/quickCss.css");
@ -70,42 +58,7 @@ function ifExistsRead(path: string): string | undefined {
if (existsSync(path)) return readFileSync(path, "utf-8"); 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 { export function registerIpc(passedWindow: BrowserWindow): void {
if (ipcRegistered) return;
ipcRegistered = true;
startThemeWatcher();
ipcMain.handle("getShelterBundle", () => { ipcMain.handle("getShelterBundle", () => {
return { return {
js: ifExistsRead(path.join(app.getPath("userData"), "shelter.js")), js: ifExistsRead(path.join(app.getPath("userData"), "shelter.js")),
@ -128,11 +81,13 @@ export function registerIpc(passedWindow: BrowserWindow): void {
}); });
ipcMain.handle("getCustomBundle", () => { ipcMain.handle("getCustomBundle", () => {
const enabled = getConfig("mods").includes("custom"); const enabled = getConfig("mods").includes("custom");
return { if (enabled) {
js: enabled ? ifExistsRead(path.join(app.getPath("userData"), "custom.js")) : undefined, return {
css: enabled ? ifExistsRead(path.join(app.getPath("userData"), "custom.css")) : undefined, js: ifExistsRead(path.join(app.getPath("userData"), "custom.js")),
enabled, css: ifExistsRead(path.join(app.getPath("userData"), "custom.css")),
}; enabled,
};
}
}); });
// theming // theming
@ -210,16 +165,26 @@ export function registerIpc(passedWindow: BrowserWindow): void {
}); });
ipcMain.on("getThemes", (event) => { ipcMain.on("getThemes", (event) => {
event.returnValue = getCachedThemeList(); const themes = [];
}); const themeFolders = readdirSync(themesPath);
ipcMain.on("refreshThemesCache", (event) => { for (const folder of themeFolders) {
invalidateThemeListCache(); if (existsSync(`${themesPath}/${folder}/manifest.json`)) {
event.returnValue = getCachedThemeList(); const manifest = JSON.parse(
readFileSync(`${themesPath}/${folder}/manifest.json`, "utf8"),
) as ThemeManifest;
themes.push({ ...manifest, id: folder });
}
}
event.returnValue = themes;
}); });
ipcMain.on("splashEnd", () => { ipcMain.on("splashEnd", () => {
splashWindow?.close(); splashWindow.close();
applyStartupWindowVisibility(passedWindow); if (getConfig("startMinimized")) {
passedWindow.hide();
} else {
passedWindow.show();
}
}); });
ipcMain.on("setLang", (_event, lang: string) => { ipcMain.on("setLang", (_event, lang: string) => {
setLang(lang); setLang(lang);
@ -267,7 +232,7 @@ export function registerIpc(passedWindow: BrowserWindow): void {
passedWindow.unmaximize(); passedWindow.unmaximize();
}); });
ipcMain.on("win-show", () => { ipcMain.on("win-show", () => {
revealWindow(passedWindow); passedWindow.show();
}); });
ipcMain.on("win-hide", () => { ipcMain.on("win-hide", () => {
passedWindow.hide(); passedWindow.hide();
@ -351,9 +316,6 @@ export function registerIpc(passedWindow: BrowserWindow): void {
shell.showItemInFolder(themesPath); shell.showItemInFolder(themesPath);
}); });
ipcMain.on("openPluginsFolder", () => { ipcMain.on("openPluginsFolder", () => {
shell.showItemInFolder(extensionsPath);
});
ipcMain.on("openRuntimePluginsFolder", () => {
shell.showItemInFolder(pluginsPath); shell.showItemInFolder(pluginsPath);
}); });
ipcMain.on("openCrashesFolder", () => { ipcMain.on("openCrashesFolder", () => {
@ -382,12 +344,6 @@ export function registerIpc(passedWindow: BrowserWindow): void {
ipcMain.on("copyGPUInfo", () => { ipcMain.on("copyGPUInfo", () => {
clipboard.writeText(JSON.stringify(app.getGPUFeatureStatus())); 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", () => { ipcMain.on("openCustomIconDialog", () => {
dialog dialog
.showOpenDialog({ .showOpenDialog({
@ -411,25 +367,6 @@ export function registerIpc(passedWindow: BrowserWindow): void {
ipcMain.on("getProcessList", (event) => { ipcMain.on("getProcessList", (event) => {
event.returnValue = processList; 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 // custom detectables control
ipcMain.on("refreshProcessList", () => { ipcMain.on("refreshProcessList", () => {
@ -514,7 +451,6 @@ export function registerIpc(passedWindow: BrowserWindow): void {
const zipBuf = buildBackupZipBuffer(payload, { const zipBuf = buildBackupZipBuffer(payload, {
userDataPath, userDataPath,
themesPath, themesPath,
extensionsPath,
pluginsPath, pluginsPath,
pluginStoragePath, pluginStoragePath,
quickCssPath, quickCssPath,
@ -555,7 +491,6 @@ export function registerIpc(passedWindow: BrowserWindow): void {
const { clientMods } = applyBackupFromMap(map, { const { clientMods } = applyBackupFromMap(map, {
userDataPath, userDataPath,
themesPath, themesPath,
extensionsPath,
pluginsPath, pluginsPath,
pluginStoragePath, pluginStoragePath,
quickCssPath, quickCssPath,

View file

@ -1,10 +1,9 @@
import { app, BrowserWindow, Menu, type MenuItemConstructorOptions } from "electron"; import { BrowserWindow, Menu, type MenuItemConstructorOptions, app } from "electron";
import type { Keybind, KeybindActions } from "../@types/keybind.js"; import type { Keybind, KeybindActions } from "../@types/keybind.js";
import { getConfig } from "../common/config.js"; import { getConfig } from "../common/config.js";
import { setForceQuit } from "../common/forceQuit.js"; import { setForceQuit } from "../common/forceQuit.js";
import { openSettings, runAction } from "../common/keybindActions.js"; import { runAction } from "../common/keybindActions.js";
import { getLang } from "../common/lang.js"; import { getLang } from "../common/lang.js";
import { revealWindow } from "../common/windowVisibility.js";
import { mainWindows } from "./window.js"; import { mainWindows } from "./window.js";
const keybindActionLabels: Record<KeybindActions, string> = { const keybindActionLabels: Record<KeybindActions, string> = {
@ -16,7 +15,6 @@ const keybindActionLabels: Record<KeybindActions, string> = {
navigateBack: "keybind-navigateBack", navigateBack: "keybind-navigateBack",
runJavascript: "keybind-runJavascript", runJavascript: "keybind-runJavascript",
openQuickCss: "keybind-openQuickCss", openQuickCss: "keybind-openQuickCss",
openSettings: "keybind-openSettings",
}; };
export function setMenu(): void { export function setMenu(): void {
@ -54,8 +52,17 @@ export function setMenu(): void {
accelerator: "Cmd+,", accelerator: "Cmd+,",
click() { click() {
mainWindows.forEach((mainWindow) => { mainWindows.forEach((mainWindow) => {
revealWindow(mainWindow); mainWindow.show();
openSettings(); 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
}); });
}, },
}, },

View file

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

View file

@ -1,5 +1,4 @@
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require("electron");
import type { Game } from "arrpc"; import type { Game } from "arrpc";
import type { Keybind } from "../../@types/keybind.js"; import type { Keybind } from "../../@types/keybind.js";
import type { LegcordWindow } from "../../@types/legcordWindow.d.ts"; import type { LegcordWindow } from "../../@types/legcordWindow.d.ts";
@ -13,20 +12,6 @@ interface IPCSources {
name: string; name: string;
thumbnail: HTMLCanvasElement; 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", { contextBridge.exposeInMainWorld("legcord", {
window: { window: {
@ -51,8 +36,6 @@ contextBridge.exposeInMainWorld("legcord", {
openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"), openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"),
copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"), copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"),
copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"), copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"),
openWebRTCInternals: () => ipcRenderer.send("openWebRTCInternals"),
openGPUInfo: () => ipcRenderer.send("openGPUInfo"),
dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput, dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput,
}, },
touchbar: { touchbar: {
@ -104,7 +87,6 @@ contextBridge.exposeInMainWorld("legcord", {
uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id), uninstall: (id: string) => ipcRenderer.send("uninstallTheme", id),
edit: (id: string) => ipcRenderer.send("editTheme", id), edit: (id: string) => ipcRenderer.send("editTheme", id),
getThemes: () => ipcRenderer.sendSync("getThemes") as ThemeManifest[], getThemes: () => ipcRenderer.sendSync("getThemes") as ThemeManifest[],
refresh: () => ipcRenderer.sendSync("refreshThemesCache") as ThemeManifest[],
openImportPicker: () => ipcRenderer.send("openImportPicker"), openImportPicker: () => ipcRenderer.send("openImportPicker"),
set: (id: string, state: boolean) => ipcRenderer.send("setThemeEnabled", id, state), set: (id: string, state: boolean) => ipcRenderer.send("setThemeEnabled", id, state),
folder: (id: string) => ipcRenderer.send("openThemeFolder", id), folder: (id: string) => ipcRenderer.send("openThemeFolder", id),
@ -128,13 +110,6 @@ contextBridge.exposeInMainWorld("legcord", {
ipcRenderer.invoke("backupSave", data) as Promise<{ ok: true } | { ok: false; error: string }>, ipcRenderer.invoke("backupSave", data) as Promise<{ ok: true } | { ok: false; error: string }>,
restore: () => ipcRenderer.invoke("backupRestore") as Promise<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: { fs: {
/** /**
* Write a file in this plugin's scoped storage (e.g. "cache/deleted-messages.json"). * Write a file in this plugin's scoped storage (e.g. "cache/deleted-messages.json").

View file

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

View file

@ -1,11 +1,9 @@
const { ipcRenderer, webFrame } = require("electron"); const { ipcRenderer, webFrame } = require("electron");
import type { ModBundle } from "../../../@types/ModBundle.js"; import type { ModBundle } from "../../../@types/ModBundle.js";
async function inject() { async function inject() {
try { try {
await ipcRenderer.invoke("getCustomBundle").then(async (bundle: ModBundle) => { await ipcRenderer.invoke("getCustomBundle").then(async (bundle: ModBundle) => {
if (bundle?.enabled) { if (bundle.enabled) {
await webFrame.executeJavaScript(bundle.js); await webFrame.executeJavaScript(bundle.js);
if (bundle.css) { if (bundle.css) {
webFrame.insertCSS(bundle.css); //NOTE - Custom mods might require CSS. webFrame.insertCSS(bundle.css); //NOTE - Custom mods might require CSS.

View file

@ -1,11 +1,9 @@
const { ipcRenderer, webFrame } = require("electron"); const { ipcRenderer, webFrame } = require("electron");
import type { ModBundle } from "../../../@types/ModBundle.js"; import type { ModBundle } from "../../../@types/ModBundle.js";
async function inject() { async function inject() {
try { try {
await ipcRenderer.invoke("getEquicordBundle").then(async (bundle: ModBundle) => { await ipcRenderer.invoke("getEquicordBundle").then(async (bundle: ModBundle) => {
if (bundle?.enabled) { if (bundle.enabled) {
await webFrame.executeJavaScript(bundle.js); await webFrame.executeJavaScript(bundle.js);
webFrame.insertCSS(bundle.css!); //NOTE - Equicord requires CSS. webFrame.insertCSS(bundle.css!); //NOTE - Equicord requires CSS.
} }

View file

@ -1,5 +1,4 @@
const { ipcRenderer, webFrame } = require("electron"); const { ipcRenderer, webFrame } = require("electron");
import type { ModBundle } from "../../../@types/ModBundle.js"; import type { ModBundle } from "../../../@types/ModBundle.js";
const requiredPlugins: Record<string, [string, { isVisible: boolean; allowedActions: Record<string, true> }]> = { const requiredPlugins: Record<string, [string, { isVisible: boolean; allowedActions: Record<string, true> }]> = {
@ -18,7 +17,7 @@ if (process.platform === "darwin") {
async function inject() { async function inject() {
try { try {
await ipcRenderer.invoke("getShelterBundle").then(async (bundle: ModBundle) => { await ipcRenderer.invoke("getShelterBundle").then(async (bundle: ModBundle) => {
if (bundle?.enabled) { if (bundle.enabled) {
await webFrame.executeJavaScript(`(()=>{ await webFrame.executeJavaScript(`(()=>{
const SHELTER_INJECTOR_PLUGINS = ${JSON.stringify(requiredPlugins)}; const SHELTER_INJECTOR_PLUGINS = ${JSON.stringify(requiredPlugins)};
${bundle.js} ${bundle.js}

View file

@ -1,11 +1,9 @@
const { ipcRenderer, webFrame } = require("electron"); const { ipcRenderer, webFrame } = require("electron");
import type { ModBundle } from "../../../@types/ModBundle.js"; import type { ModBundle } from "../../../@types/ModBundle.js";
async function inject() { async function inject() {
try { try {
await ipcRenderer.invoke("getVencordBundle").then(async (bundle: ModBundle) => { await ipcRenderer.invoke("getVencordBundle").then(async (bundle: ModBundle) => {
if (bundle?.enabled) { if (bundle.enabled) {
await webFrame.executeJavaScript(bundle.js); await webFrame.executeJavaScript(bundle.js);
webFrame.insertCSS(bundle.css!); //NOTE - Vencord requires CSS. webFrame.insertCSS(bundle.css!); //NOTE - Vencord requires CSS.
} }

View file

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

View file

@ -1,10 +1,18 @@
type OptimizableFunction<T extends Node> = (child: T) => T; type OptimizableFunction<T extends Node> = (child: T) => T;
const optimize = <T extends Node>(orig: OptimizableFunction<T>) => { const optimize = <T extends Node>(orig: OptimizableFunction<T>) => {
return function (this: Element, ...args: [Element]): 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 orig.apply(this, args as unknown as [T]); return orig.apply(this, args as unknown as [T]);
} as unknown as OptimizableFunction<T>; } as unknown as OptimizableFunction<T>;
}; };
// We are taking in the function itself
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
Element.prototype.removeChild = optimize(Element.prototype.removeChild); Element.prototype.removeChild = optimize(Element.prototype.removeChild);
// Thanks Ari - <@1249446413952225452>

View file

@ -1,6 +1,5 @@
import { addScript, addStyle, injectJS } from "../../common/dom.js"; import { addScript, addStyle, injectJS } from "../../common/dom.js";
import { sleep } from "../../common/sleep.js"; import { sleep } from "../../common/sleep.js";
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
const version = ipcRenderer.sendSync("displayVersion") as string; const version = ipcRenderer.sendSync("displayVersion") as string;
@ -30,123 +29,15 @@ 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 ~12.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 // 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. // (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 // 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. // releases the hardware, and falls back to the original behavior if "exact" fails.
// Injected into the page context because contextIsolation is enabled. // Injected into the page context because contextIsolation is enabled.
// See: https://github.com/electron/electron/issues/44502 // 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"); const cameraFixScript = document.createElement("script");
cameraFixScript.textContent = `(function() { cameraFixScript.textContent = `(function() {
var legcordStopPrevAudioStreams = ${stopPrevAudioStreams};
var _origGUM = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); var _origGUM = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
var _activeVideoStreams = []; var _activeVideoStreams = [];
var _activeAudioStreams = []; var _activeAudioStreams = [];
@ -167,16 +58,16 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
function trackStream(stream) { function trackStream(stream) {
var ref = new WeakRef(stream); var ref = new WeakRef(stream);
if (stream.getVideoTracks().length > 0) _activeVideoStreams.push(ref); if (stream.getVideoTracks().length > 0) _activeVideoStreams.push(ref);
if (legcordStopPrevAudioStreams && stream.getAudioTracks().length > 0) _activeAudioStreams.push(ref); if (stream.getAudioTracks().length > 0) _activeAudioStreams.push(ref);
} }
navigator.mediaDevices.getUserMedia = async function(constraints) { navigator.mediaDevices.getUserMedia = async function(constraints) {
var hasVideo = constraints && constraints.video && typeof constraints.video !== "boolean"; var hasVideo = constraints && constraints.video && typeof constraints.video !== "boolean";
var hasAudio = constraints && constraints.audio && typeof constraints.audio !== "boolean"; var hasAudio = constraints && constraints.audio && typeof constraints.audio !== "boolean";
// Release previous hardware when new request comes in for the same kind (audio: darwin only) // Release previous hardware when new request comes in for the same kind
if (hasVideo && _activeVideoStreams.length > 0) stopTrackedStreams(_activeVideoStreams, "video"); if (hasVideo && _activeVideoStreams.length > 0) stopTrackedStreams(_activeVideoStreams, "video");
if (legcordStopPrevAudioStreams && hasAudio && _activeAudioStreams.length > 0) stopTrackedStreams(_activeAudioStreams, "audio"); if (hasAudio && _activeAudioStreams.length > 0) stopTrackedStreams(_activeAudioStreams, "audio");
var hasStringVideoDeviceId = hasVideo && typeof constraints.video.deviceId === "string"; var hasStringVideoDeviceId = hasVideo && typeof constraints.video.deviceId === "string";
if (!hasStringVideoDeviceId) { if (!hasStringVideoDeviceId) {
@ -236,9 +127,40 @@ 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() { async function load() {
await sleep(5000).then(() => { await sleep(5000).then(() => {
// Venmic audio injection lives in the Shelter screenshare getDisplayMedia patch. 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;
};
// dirty hack to make clicking notifications focus Legcord // dirty hack to make clicking notifications focus Legcord
addScript(` addScript(`
(() => { (() => {
@ -276,7 +198,6 @@ async function load() {
el.id = "ac-ver"; el.id = "ac-ver";
el.textContent = `Legcord Version: ${version}`; el.textContent = `Legcord Version: ${version}`;
info.after(el); info.after(el);
observer.disconnect();
}); });
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
} }

View file

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

View file

@ -4,12 +4,10 @@ import "./mods/shelter.mjs";
import "./mods/vencord.mjs"; import "./mods/vencord.mjs";
import "./mods/equicord.mjs"; import "./mods/equicord.mjs";
import "./mods/custom.mjs"; import "./mods/custom.mjs";
import "./plugins.mjs";
import "./patches.mjs"; import "./patches.mjs";
import "./newTitlebar.mjs"; import "./newTitlebar.mjs";
import "./titlebar.mjs"; import "./titlebar.mjs";
import "./themes.js"; import "./themes.js";
import "./inviteBackButton.mjs";
console.log("Legcord"); console.log("Legcord");
window.localStorage.setItem("hideNag", "true"); window.localStorage.setItem("hideNag", "true");

View file

@ -1,5 +1,4 @@
import { addTheme } from "../../common/dom.js"; import { addTheme } from "../../common/dom.js";
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
ipcRenderer.on("addTheme", (_event: unknown, name: string, css: string) => { ipcRenderer.on("addTheme", (_event: unknown, name: string, css: string) => {

View file

@ -1,8 +1,6 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
import type { Settings } from "../../@types/settings.js"; import type { Settings } from "../../@types/settings.js";
import { addStyle } from "../../common/dom.js"; import { addStyle } from "../../common/dom.js";
const titlebarHTML = `<nav class="titlebar"> const titlebarHTML = `<nav class="titlebar">
<div class="window-title" id="window-title"></div> <div class="window-title" id="window-title"></div>
<div id="window-controls-container"> <div id="window-controls-container">

View file

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

View file

@ -1,4 +1,4 @@
import { desktopCapturer, ipcMain, type Streams, session } from "electron"; import { type Streams, desktopCapturer, ipcMain, session } from "electron";
import { getConfig } from "../common/config.js"; import { getConfig } from "../common/config.js";
import { mainWindows } from "./window.js"; import { mainWindows } from "./window.js";
@ -17,13 +17,12 @@ export function registerCustomHandler(): void {
console.log("WebRTC Capturer detected, using native window picker."); console.log("WebRTC Capturer detected, using native window picker.");
if (sources[0] === undefined) return callback({}); if (sources[0] === undefined) return callback({});
} }
ipcMain.removeAllListeners("startScreenshare");
ipcMain.once("startScreenshare", (_event, id: string, name: string, audio: boolean) => { ipcMain.once("startScreenshare", (_event, id: string, name: string, audio: boolean) => {
console.log(`ID: ${id}`); console.log(`ID: ${id}`);
if (id === "none") { if (id === "none") {
try { try {
callback({}); callback({});
} catch (_e) {} } catch (e) {}
} else { } else {
console.log(`Audio status: ${audio}`); console.log(`Audio status: ${audio}`);
const result = { id, name }; const result = { id, name };
@ -32,7 +31,6 @@ export function registerCustomHandler(): void {
switch (process.platform) { switch (process.platform) {
case "win32": case "win32":
case "linux": case "linux":
case "darwin":
options = { video: result }; options = { video: result };
if (audio) if (audio)
options = { options = {
@ -48,7 +46,6 @@ export function registerCustomHandler(): void {
}); });
mainWindows.every((window) => { mainWindows.every((window) => {
window.webContents.send("getSources", sources); window.webContents.send("getSources", sources);
return true;
}); });
}, },
{ useSystemPicker: getConfig("useMacSystemPicker") }, { useSystemPicker: getConfig("useMacSystemPicker") },

View file

@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { app, nativeImage, TouchBar } from "electron"; import { TouchBar, app, nativeImage } from "electron";
import { navigateTo } from "../common/dom.js"; import { navigateTo } from "../common/dom.js";
import { deafenToggle, leaveCall, muteToggle } from "../common/keybindActions.js"; import { deafenToggle, leaveCall, muteToggle } from "../common/keybindActions.js";
import { getLang } from "../common/lang.js"; import { getLang } from "../common/lang.js";

Some files were not shown because too many files have changed in this diff Show more