Compare commits

..

No commits in common. "e9a95d4c751d24cf0d8f68267b2e92de0b9e1e24" and "b2151296a4feb72f91e895e8ff2f5bb93180bb77" have entirely different histories.

27 changed files with 453 additions and 1512 deletions

1
.gitignore vendored
View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

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

@ -78,8 +78,6 @@
"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": "Scan for running games to improve Rich Presence detection. Disable to reduce CPU usage. Requires a restart.",
"settings-windowsLegacyScanning": "Windows legacy scanning", "settings-windowsLegacyScanning": "Windows legacy scanning",
@ -246,13 +244,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",
@ -268,8 +263,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",

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,7 +1,6 @@
import type { Configuration } from "electron-builder"; import type { Configuration } from "electron-builder";
import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs"; import { applyAppImageSandboxFix } from "./scripts/build/sandboxFix.mjs";
import debianLicence from "./scripts/spdxLicenceDebianFormat";
import { ACTION_FRIENDLY_NAMES, EXCLUDED_FROM_SHORTCUTS, ValidActions } from "./src/common/commandDefinitions"; import { ACTION_FRIENDLY_NAMES, EXCLUDED_FROM_SHORTCUTS, ValidActions } from "./src/common/commandDefinitions";
const desktopActions = (exec: "AppRun" | "/opt/Legcord/legcord") => const desktopActions = (exec: "AppRun" | "/opt/Legcord/legcord") =>
@ -72,7 +71,7 @@ export const config: Configuration = {
}, },
pacman: { pacman: {
depends: ["gtk3", "libnotify", "xdg-utils", "at-spi2-core", "alsa-lib", "nspr", "nss"], depends: ["gtk3", "libnotify", "nss", "libxss", "libxtst", "xdg-utils", "at-spi2-core", "libsecret"],
}, },
nsis: { nsis: {
@ -102,14 +101,13 @@ 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: { desktop: {
entry: { entry: {
Actions: availableActions, Actions: availableActions,
}, },
desktopActions: desktopActions("/opt/Legcord/legcord"), desktopActions: desktopActions("/opt/Legcord/legcord"),
}, },
fpm: [`${debianLicence()}=/usr/share/doc/legcord/copyright`],
}, },
files: [ files: [

View file

@ -40,19 +40,17 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@uwu/lune": "^1.6.2", "@uwu/lune": "^1.6.2",
"@uwu/shelter-defs": "^1.5.1", "@uwu/shelter-defs": "^1.5.1",
"@uwu/shelter-ui": "^0.0.6",
"@xmldom/xmldom": "^0.9.10", "@xmldom/xmldom": "^0.9.10",
"app-builder-lib": "^26.8.1", "app-builder-lib": "^26.8.1",
"babel-preset-solid": "^1.9.12", "babel-preset-solid": "^1.9.12",
"electron": "43.2.0", "electron": "43.2.0",
"electron-builder": "26.15.7", "electron-builder": "26.8.1",
"lucide-solid": "^1.16.0", "lucide-solid": "^1.16.0",
"monaco-editor": "0.52.2", "monaco-editor": "0.52.2",
"rolldown": "1.2.0", "rolldown": "1.0.3",
"rollup-plugin-copy": "^3.5.0", "rollup-plugin-copy": "^3.5.0",
"solid-js": "^1.9.14", "solid-js": "^1.9.14",
"solid-motionone": "^1.0.4", "solid-motionone": "^1.0.4",
"spdx-license-list": "^6.11.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"xml-formatter": "^3.7.0" "xml-formatter": "^3.7.0"
}, },

565
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -31,8 +31,6 @@ 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/" },

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

@ -61,8 +61,6 @@ export interface Settings {
startMinimized: boolean; startMinimized: boolean;
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;

View file

@ -12,7 +12,7 @@ let configCache: Settings | null = null;
let configCacheTime = 0; let configCacheTime = 0;
const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds
const defaults: Settings = { const defaults: Settings = {
windowStyle: "overlay", windowStyle: "default",
channel: "stable", channel: "stable",
bounceOnPing: false, bounceOnPing: false,
csp: "none", csp: "none",
@ -34,12 +34,11 @@ 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,
@ -82,7 +81,6 @@ 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: "",
@ -122,11 +120,6 @@ export function getConfig<K extends keyof Settings>(object: K): Settings[K] {
configCacheTime = now; configCacheTime = now;
return returnData[object]; return returnData[object];
} }
/** True when the settings toggle is on or `--start-minimized` was passed for this session. */
export function shouldStartMinimized(): boolean {
return process.argv.includes("--start-minimized") || getConfig("startMinimized") === true;
}
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;

View file

@ -1,13 +1,7 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { import { interface as dbusInterface, type ProxyInterface, sessionBus, Variant } from "@jellybrick/dbus-next";
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 { ACTION_FRIENDLY_NAMES, EXCLUDED_FROM_SHORTCUTS, ValidActions } from "./common/commandDefinitions";
import { handleAction, isValidAction } from "./common/handleCommands"; import { handleAction, isValidAction } from "./common/handleCommands";
@ -19,8 +13,6 @@ export const DBUS_ADDRESS = "/app/legcord/Legcord";
const FREEDESKTOP_PORTAL_NAME = "org.freedesktop.portal.Desktop"; const FREEDESKTOP_PORTAL_NAME = "org.freedesktop.portal.Desktop";
const FREEDESKTOP_PORTAL_ADDRESS = "/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 // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html
type Shortcuts = [string, { description: Variant<string> }]; type Shortcuts = [string, { description: Variant<string> }];
interface GlobalShortcuts extends ProxyInterface { interface GlobalShortcuts extends ProxyInterface {
@ -82,16 +74,8 @@ class LegcordInterface extends Interface {
} }
} }
let bus: MessageBus | undefined; const bus = sessionBus();
let legcordInterface: LegcordInterface | undefined; var legcordInterface = new LegcordInterface();
function getBus(): MessageBus {
if (!bus) {
bus = sessionBus();
legcordInterface = new LegcordInterface();
}
return bus;
}
function ensureDesktopFile(): void { function ensureDesktopFile(): void {
const dir = join(homedir(), ".local/share/applications"); const dir = join(homedir(), ".local/share/applications");
@ -104,18 +88,14 @@ function ensureDesktopFile(): void {
function registerAppId(): Promise<void> { function registerAppId(): Promise<void> {
ensureDesktopFile(); // we need a desktop file at XDG_PATH so we can associate with legcord's app_id ensureDesktopFile(); // we need a desktop file at XDG_PATH so we can associate with legcord's app_id
return getBus() return bus.getProxyObject("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop").then((portalObj) => {
.getProxyObject("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop") const registry: RegistryInterface = portalObj.getInterface("org.freedesktop.host.portal.Registry");
.then((portalObj) => { return registry.Register(DBUS_INTERFACE_NAME, {});
const registry: RegistryInterface = portalObj.getInterface("org.freedesktop.host.portal.Registry"); });
return registry.Register(DBUS_INTERFACE_NAME, {});
});
} }
export async function setupGlobalShortcuts() { export async function setupGlobalShortcuts() {
if (!isLinux) return; const portalObj = await bus.getProxyObject(FREEDESKTOP_PORTAL_NAME, FREEDESKTOP_PORTAL_ADDRESS);
const portalObj = await getBus().getProxyObject(FREEDESKTOP_PORTAL_NAME, FREEDESKTOP_PORTAL_ADDRESS);
const properties: PropertiesInterface = portalObj.getInterface("org.freedesktop.DBus.Properties"); const properties: PropertiesInterface = portalObj.getInterface("org.freedesktop.DBus.Properties");
const globalShortcuts: GlobalShortcuts = portalObj.getInterface("org.freedesktop.portal.GlobalShortcuts"); const globalShortcuts: GlobalShortcuts = portalObj.getInterface("org.freedesktop.portal.GlobalShortcuts");
@ -128,19 +108,17 @@ export async function setupGlobalShortcuts() {
console.debug(`Registered ${DBUS_INTERFACE_NAME} in freedesktop Registry.`); console.debug(`Registered ${DBUS_INTERFACE_NAME} in freedesktop Registry.`);
function awaitResponse(requestPath: string): Promise<Record<string, unknown>> { function awaitResponse(requestPath: string): Promise<Record<string, unknown>> {
return getBus() return bus.getProxyObject(FREEDESKTOP_PORTAL_NAME, requestPath).then(
.getProxyObject(FREEDESKTOP_PORTAL_NAME, requestPath) (requestObj) =>
.then( new Promise<Record<string, unknown>>((resolve, reject) => {
(requestObj) => const requestIface = requestObj.getInterface("org.freedesktop.portal.Request");
new Promise<Record<string, unknown>>((resolve, reject) => {
const requestIface = requestObj.getInterface("org.freedesktop.portal.Request");
requestIface.once("Response", (responseCode: number, results: Record<string, unknown>) => { requestIface.once("Response", (responseCode: number, results: Record<string, unknown>) => {
if (responseCode === 0) resolve(results); if (responseCode === 0) resolve(results);
else reject(new Error(`Request failed with code ${responseCode}`)); else reject(new Error(`Request failed with code ${responseCode}`));
}); });
}), }),
); );
} }
const sessionRequestPath = await globalShortcuts.CreateSession({ const sessionRequestPath = await globalShortcuts.CreateSession({
@ -168,19 +146,13 @@ export async function setupGlobalShortcuts() {
} }
export async function startDbusService(): Promise<void> { export async function startDbusService(): Promise<void> {
if (!isLinux) return; await bus.requestName(DBUS_INTERFACE_NAME);
bus.export(DBUS_ADDRESS, legcordInterface);
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.info(`Registered DBus service at ${DBUS_INTERFACE_NAME} ${DBUS_ADDRESS}`);
// console.debug(legcordInterface) // console.debug(legcordInterface)
} }
export function disconnectDbusService(): void { export function disconnectDbusService(): void {
if (!bus) return;
bus.disconnect(); bus.disconnect();
bus = undefined;
legcordInterface = undefined;
} }

View file

@ -18,7 +18,7 @@ import {
unblacklistGame as blacklistGameRemove, unblacklistGame as blacklistGameRemove,
getBlacklist, getBlacklist,
} from "../common/blacklistGame.js"; } from "../common/blacklistGame.js";
import { getConfig, getConfigLocation, setConfig, setConfigBulk, shouldStartMinimized } 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 {
@ -217,8 +217,8 @@ export function registerIpc(passedWindow: BrowserWindow): void {
}); });
ipcMain.on("splashEnd", () => { ipcMain.on("splashEnd", () => {
splashWindow?.close(); splashWindow.close();
if (shouldStartMinimized()) { if (getConfig("startMinimized")) {
passedWindow.hide(); passedWindow.hide();
} else { } else {
passedWindow.show(); passedWindow.show();

View file

@ -36,11 +36,7 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
// Discord's Go Live answer forces profile-level-id=42e01f (OpenH264); local-only munging // 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. // 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. // 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"); const bitrateScript = document.createElement("script");
bitrateScript.textContent = `(function () { bitrateScript.textContent = `(function () {
var CAP = "80000"; var CAP = "80000";
@ -49,7 +45,7 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
var MIN_BR = "3000"; var MIN_BR = "3000";
var START_BR = "3000"; var START_BR = "3000";
// VideoToolbox (macOS) and Media Foundation (Windows) HW encode avoid OpenH264 CBP. // VideoToolbox (macOS) and Media Foundation (Windows) HW encode avoid OpenH264 CBP.
var preferHwH264 = ${preferHwH264 ? "true" : "false"}; var preferHwH264 = ${process.platform === "darwin" || process.platform === "win32" ? "true" : "false"};
function setOrAppendFmtpParam(sdp, key, value) { function setOrAppendFmtpParam(sdp, key, value) {
var re = new RegExp(key + "=\\\\d+", "g"); var re = new RegExp(key + "=\\\\d+", "g");
if (sdp.indexOf(key + "=") !== -1) { if (sdp.indexOf(key + "=") !== -1) {

View file

@ -14,7 +14,7 @@ import {
shell, shell,
} from "electron"; } from "electron";
import contextMenu from "electron-context-menu"; import contextMenu from "electron-context-menu";
import { firstRun, getConfig, setConfig, shouldStartMinimized } from "../common/config.js"; import { firstRun, getConfig, setConfig } from "../common/config.js";
import { navigateTo } from "../common/dom.js"; import { navigateTo } from "../common/dom.js";
import { forceQuit, setForceQuit } from "../common/forceQuit.js"; import { forceQuit, setForceQuit } from "../common/forceQuit.js";
import { handleCommands, passedValidArgument } from "../common/handleCommands.js"; import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
@ -474,13 +474,8 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
break; break;
} }
// When splash won't run, finalize visibility here (splashEnd never fires). if (getConfig("skipSplash")) {
if (getConfig("skipSplash") || shouldStartMinimized()) { passedWindow.show();
if (shouldStartMinimized()) {
passedWindow.hide();
} else {
passedWindow.show();
}
} }
} }

View file

@ -15,7 +15,6 @@ import {
setConfig, setConfig,
setFirstRun, setFirstRun,
setup, setup,
shouldStartMinimized,
} from "./common/config.js"; } from "./common/config.js";
import { getPreset } from "./common/flags.js"; import { getPreset } from "./common/flags.js";
import { setLang } from "./common/lang.js"; import { setLang } from "./common/lang.js";
@ -109,9 +108,7 @@ function args(): void {
export async function init(): Promise<void> { export async function init(): Promise<void> {
// Skip setup if bypass flag was used // Skip setup if bypass flag was used
if (bypassSetup || !(firstRun === true || undefined)) { if (bypassSetup || !(firstRun === true || undefined)) {
// Splash owns the final show/hide via splashEnd; skip it when splash is disabled if (getConfig("skipSplash") === false) {
// or when we intentionally start in the background.
if (getConfig("skipSplash") === false && !shouldStartMinimized()) {
void createSplashWindow(); // NOTE - Awaiting will hang at start void createSplashWindow(); // NOTE - Awaiting will hang at start
} }
createWindow(); createWindow();

View file

@ -12,8 +12,8 @@ export async function createSetupWindow(): Promise<void> {
if (platform() !== "darwin") import("./tray.js"); if (platform() !== "darwin") import("./tray.js");
return new Promise(() => { return new Promise(() => {
const windowOptions: BrowserWindowConstructorOptions = { const windowOptions: BrowserWindowConstructorOptions = {
width: 840, width: 800,
height: 720, height: 600,
title: getLang("setup-windowTitle"), title: getLang("setup-windowTitle"),
darkTheme: true, darkTheme: true,
icon: getConfig("customIcon") ?? path.join(import.meta.dirname, "../", "/assets/desktop.png"), icon: getConfig("customIcon") ?? path.join(import.meta.dirname, "../", "/assets/desktop.png"),

View file

@ -15,7 +15,17 @@ contextBridge.exposeInMainWorld("setup", {
if (ipcRenderer.sendSync("setup-getOS") !== "darwin") { if (ipcRenderer.sendSync("setup-getOS") !== "darwin") {
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.body.classList.add("has-setup-bg"); const css = document.createElement("style");
css.innerHTML = `.bg {
background-image: url("legcord://assets/mockup.jpg");
background-repeat: round;
height: 100%;
width: 100%;
position: absolute;
outline: 80px solid black;
-webkit-filter: blur(10px);
}`;
document.head.appendChild(css);
}); });
} }

View file

@ -1,488 +0,0 @@
:root {
--font-primary: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-display: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-headline: "ABC Ginto Nord", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--saturation-factor: 1;
/* Soft Legcord purple accents (kept quiet) */
--setup-purple: var(--blurple-50);
--setup-purple-soft: rgba(88, 101, 242, 0.14);
--setup-purple-glow: rgba(114, 137, 218, 0.18);
/* Discord Mana tokens (from stable web stylesheets / theme-dark) */
--blurple-50-hsl: 234.935 calc(var(--saturation-factor, 1) * 85.556%) 64.706%;
--blurple-60-hsl: 232.941 calc(var(--saturation-factor, 1) * 46.667%) 50%;
--blurple-65-hsl: 232 calc(var(--saturation-factor, 1) * 47.511%) 43.333%;
--blurple-50: hsl(var(--blurple-50-hsl) / 1);
--blurple-60: hsl(var(--blurple-60-hsl) / 1);
--blurple-65: hsl(var(--blurple-65-hsl) / 1);
--neutral-1: #fff;
--opacity-white-8: hsl(0 0% 100% / 0.0784313725490196);
--opacity-4: hsl(0 0% 100% / 0.0392156862745098);
--opacity-12: hsl(0 0% 100% / 0.12156862745098039);
--opacity-16: hsl(0 0% 100% / 0.1607843137254902);
--opacity-20: hsl(0 0% 100% / 0.2);
/* shelter-ui → Discord Mana control colors */
--control-primary-background-default: var(--blurple-50);
--control-primary-background-hover: var(--blurple-60);
--control-primary-background-active: var(--blurple-65);
--control-primary-text-default: var(--neutral-1);
--control-primary-text-hover: var(--neutral-1);
--control-primary-text-active: var(--neutral-1);
--control-primary-border-default: var(--opacity-white-8);
--control-primary-border-hover: var(--opacity-white-8);
--control-primary-border-active: var(--opacity-white-8);
--control-secondary-background-default: var(--opacity-12);
--control-secondary-background-hover: var(--opacity-16);
--control-secondary-background-active: var(--opacity-20);
--control-secondary-text-default: var(--neutral-1);
--control-secondary-text-hover: var(--neutral-1);
--control-secondary-text-active: var(--neutral-1);
--control-secondary-border-default: var(--opacity-4);
--control-secondary-border-hover: var(--opacity-4);
--control-secondary-border-active: var(--opacity-4);
--custom-button-transition-duration: 170ms;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
font-family: var(--font-primary);
background: #1e1f22;
color: var(--text-default);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.draggable {
position: absolute;
top: 0;
width: 100%;
height: 40px;
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
z-index: 10;
}
.bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 70% 50% at 50% -8%, var(--setup-purple-glow), transparent 58%),
radial-gradient(ellipse 40% 35% at 0% 100%, rgba(88, 101, 242, 0.08), transparent 55%), #1e1f22;
}
body.has-setup-bg .bg {
background-image:
linear-gradient(180deg, rgba(30, 31, 34, 0.75), rgba(30, 31, 34, 0.9)),
radial-gradient(ellipse 60% 40% at 50% 0%, var(--setup-purple-glow), transparent 60%),
url("legcord://assets/mockup.jpg");
background-size: cover;
background-position: center;
filter: none;
}
body.has-setup-bg .bg::after {
content: "";
position: absolute;
inset: 0;
backdrop-filter: blur(18px);
background: rgba(30, 31, 34, 0.32);
}
#root {
position: relative;
z-index: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 28px;
}
.setup-shell {
position: relative;
width: 100%;
max-width: 560px;
max-height: calc(100vh - 56px);
display: flex;
flex-direction: column;
background: var(--modal-background, #313338);
border-radius: 12px;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.3),
0 0 0 1px var(--setup-purple-soft),
0 12px 28px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
.setup-shell::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--setup-purple) 35%, var(--setup-purple) 65%, transparent);
opacity: 0.55;
pointer-events: none;
z-index: 2;
}
.setup-progress {
display: flex;
gap: 4px;
padding: 16px 20px 0;
}
.setup-progress-segment {
flex: 1;
height: 4px;
border-radius: 2px;
background: rgba(88, 101, 242, 0.16);
transition: background 0.15s ease;
}
.setup-progress-segment--done,
.setup-progress-segment--active {
background: var(--setup-purple);
}
.setup-body {
padding: 22px 24px 14px;
overflow-y: auto;
flex: 1;
min-height: 0;
}
.setup-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 24px;
background: var(--modal-footer-background, #2b2d31);
border-top: 1px solid rgba(0, 0, 0, 0.25);
}
.setup-step-indicator {
color: var(--header-secondary);
font-family: var(--font-primary);
font-size: 12px;
font-weight: var(--font-weight-medium);
letter-spacing: 0.02em;
line-height: 16px;
white-space: nowrap;
}
.setup-header {
margin-bottom: 20px;
}
.setup-title {
margin: 0;
color: var(--header-primary, #f2f3f5);
font-family: var(--font-display);
font-size: 20px;
font-weight: var(--font-weight-semibold);
letter-spacing: 0.01em;
line-height: 1.2;
}
.setup-title--hero {
font-size: 32px;
font-weight: var(--font-weight-bold);
line-height: 1.15;
}
.setup-subtitle {
display: block;
margin: 8px 0 0;
color: var(--header-secondary, #b5bac1);
font-family: var(--font-primary);
font-size: 14px;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: 0;
}
.setup-subtitle--hero {
margin-top: 10px;
margin-bottom: 24px;
max-width: 360px;
font-size: 16px;
line-height: 22px;
}
.setup-welcome,
.setup-finish {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 24px 8px 28px;
}
.setup-logo {
width: 88px;
height: 88px;
margin-bottom: 20px;
border-radius: 22px;
object-fit: cover;
box-shadow:
0 0 0 1px rgba(88, 101, 242, 0.22),
0 10px 28px rgba(0, 0, 0, 0.4),
0 0 32px rgba(88, 101, 242, 0.18);
}
.setup-finish-icon {
width: 64px;
height: 64px;
margin-bottom: 18px;
}
.setup-finish-icon svg {
width: 100%;
height: 100%;
display: block;
filter: drop-shadow(0 6px 16px rgba(88, 101, 242, 0.35));
}
.setup-cta {
margin-top: 4px;
max-width: 100%;
font-family: var(--font-primary);
font-weight: var(--font-weight-medium);
letter-spacing: 0;
transition:
background-color var(--custom-button-transition-duration) ease,
color var(--custom-button-transition-duration) ease,
border-color var(--custom-button-transition-duration) ease;
}
.setup-footer-btn {
font-family: var(--font-primary);
font-weight: var(--font-weight-medium);
transition:
background-color var(--custom-button-transition-duration) ease,
color var(--custom-button-transition-duration) ease,
border-color var(--custom-button-transition-duration) ease;
}
.setup-note {
width: 100%;
margin: 0 0 22px;
padding: 12px 14px;
border-radius: 8px;
background: color-mix(in srgb, var(--setup-purple) 6%, var(--background-secondary, #2b2d31));
border: 1px solid rgba(88, 101, 242, 0.12);
color: var(--header-secondary);
text-align: left;
}
.setup-note p {
margin: 0;
font-family: var(--font-primary);
font-size: 14px;
line-height: 20px;
color: var(--header-secondary);
}
.setup-options {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 8px;
}
.setup-option {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
padding: 12px;
border-radius: 8px;
border: 1px solid transparent;
background: var(--background-secondary, #2b2d31);
color: inherit;
text-align: left;
cursor: pointer;
font-family: var(--font-primary);
outline: none;
transition:
background 0.1s ease-out,
border-color 0.1s ease-out,
box-shadow 0.1s ease-out;
-webkit-app-region: no-drag;
}
.setup-option:hover {
background: var(--background-surface-high, #393a41);
}
.setup-option:focus-visible {
box-shadow: 0 0 0 2px var(--setup-purple);
}
.setup-option--selected {
background: color-mix(in srgb, var(--setup-purple) 14%, var(--background-secondary, #2b2d31));
border-color: color-mix(in srgb, var(--setup-purple) 55%, transparent);
box-shadow: inset 0 0 0 1px rgba(88, 101, 242, 0.2);
}
.setup-option--selected:hover {
background: color-mix(in srgb, var(--setup-purple) 20%, var(--background-secondary, #2b2d31));
}
.setup-option-preview {
width: 128px;
height: 80px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
background: var(--input-background, #1e1f22);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.setup-option-preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.setup-option-icon {
width: 44px;
height: 44px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
background: var(--input-background, #1e1f22);
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.setup-option-icon--selected {
background: color-mix(in srgb, var(--setup-purple) 18%, var(--input-background, #1e1f22));
}
.setup-option-icon img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.setup-option-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.setup-option-title-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.setup-option-title {
color: var(--header-primary, #f2f3f5);
font-family: var(--font-primary);
font-size: 16px;
font-weight: var(--font-weight-semibold);
line-height: 20px;
letter-spacing: 0.01em;
}
.setup-option-desc {
color: var(--header-secondary, #b5bac1);
font-family: var(--font-primary);
font-size: 14px;
font-weight: var(--font-weight-normal);
line-height: 18px;
}
.setup-badge {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 3px;
background: var(--setup-purple);
color: #fff;
font-family: var(--font-primary);
font-size: 10px;
font-weight: var(--font-weight-bold);
letter-spacing: 0.04em;
text-transform: uppercase;
line-height: 14px;
}
.setup-radio {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid var(--interactive-normal);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition:
border-color 0.1s ease-out,
background 0.1s ease-out;
}
.setup-radio--selected {
border-color: var(--setup-purple);
background: var(--setup-purple);
}
.setup-radio-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #fff;
}
.setup-warning {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 12px;
padding: 12px 14px;
border-radius: 8px;
background: rgba(240, 178, 50, 0.1);
border: 1px solid rgba(240, 178, 50, 0.28);
color: #f0b232;
font-family: var(--font-primary);
font-size: 14px;
line-height: 18px;
}
.setup-warning-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
margin-top: 1px;
}

View file

@ -4,8 +4,18 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legcord Setup</title> <title>Legcord Setup</title>
<link rel="stylesheet" href="compat.css"> <style>
<link rel="stylesheet" href="setup.css"> .draggable {
position: absolute;
top: 0;
width: 100%;
height: 50px;
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
}
</style>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
</head> </head>
<body> <body>
<div class="draggable"></div> <div class="draggable"></div>

View file

@ -1,167 +1,122 @@
import { Button, ButtonColors, ButtonSizes, injectInternalStyles, niceScrollbarsClass } from "@uwu/shelter-ui"; import {
import { type Accessor, createMemo, createResource, createSignal, For, onMount, Show } from "solid-js"; Box,
ChevronLeft,
ChevronRight,
CircleAlert,
LaptopMinimal,
LaptopMinimalCheck,
MinimizeIcon,
Monitor,
PuzzleIcon,
Settings,
Sparkles,
} from "lucide-solid";
import { createResource, createSignal, For, Show } from "solid-js";
import { render } from "solid-js/web"; import { render } from "solid-js/web";
import { Motion } from "solid-motionone"; import { Motion } from "solid-motionone";
injectInternalStyles(); const Welcome = ({ onNext, t }: { onNext: () => void; t: () => Record<string, string> | undefined }) => (
<Motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} class="text-center">
type Lang = Record<string, string> | undefined; <Sparkles class="w-8 h-8 text-purple-400 mx-auto mb-4" />
<h1 class="text-3xl font-bold text-white mb-4 bg-gradient-to-r from-purple-400 via-white to-purple-400 bg-clip-text text-transparent">
const ctaStyle = { {t()?.["setup-welcomeTitle"] ?? "Welcome to Legcord"}
"--shltr-btn-w": "280px", </h1>
"--shltr-btn-h": "44px", <p class="text-gray-400 mb-8">
"font-size": "16px",
"line-height": "20px",
padding: "2px 24px",
} as const;
const footerBtnStyle = {
"--shltr-btn-w": "96px",
"--shltr-btn-h": "38px",
} as const;
const WarningIcon = () => (
<svg class="setup-warning-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm0 14.25a1.25 1.25 0 1 1 1.25-1.25A1.25 1.25 0 0 1 12 16.25Zm1.1-3.75h-2.2l-.35-6h2.9Z" />
</svg>
);
const OptionCard = (props: {
selected: boolean;
onSelect: () => void;
title: string;
description: string;
badge?: string;
preview?: string;
icon?: string;
}) => (
<button
type="button"
class="setup-option"
classList={{ "setup-option--selected": props.selected }}
onClick={props.onSelect}
aria-pressed={props.selected}
>
<Show when={props.preview}>
<div class="setup-option-preview">
<img src={props.preview} alt="" />
</div>
</Show>
<Show when={props.icon}>
<div class="setup-option-icon" classList={{ "setup-option-icon--selected": props.selected }}>
<img src={props.icon} alt="" />
</div>
</Show>
<div class="setup-option-content">
<div class="setup-option-title-row">
<span class="setup-option-title">{props.title}</span>
<Show when={props.badge}>
<span class="setup-badge">{props.badge}</span>
</Show>
</div>
<span class="setup-option-desc">{props.description}</span>
</div>
<span class="setup-radio" classList={{ "setup-radio--selected": props.selected }} aria-hidden="true">
<Show when={props.selected}>
<span class="setup-radio-dot" />
</Show>
</span>
</button>
);
const Progress = (props: { current: number; total: number }) => (
<div class="setup-progress" aria-hidden="true">
<For each={Array.from({ length: props.total }, (_, i) => i)}>
{(i) => (
<div
class="setup-progress-segment"
classList={{
"setup-progress-segment--done": i < props.current,
"setup-progress-segment--active": i === props.current,
}}
/>
)}
</For>
</div>
);
const Welcome = ({ onNext, t }: { onNext: () => void; t: () => Lang }) => (
<Motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} class="setup-welcome">
<img class="setup-logo" src="legcord://assets/desktop.png" alt="" />
<h1 class="setup-title setup-title--hero">{t()?.["setup-welcomeTitle"] ?? "Welcome to Legcord"}</h1>
<p class="setup-subtitle setup-subtitle--hero">
{t()?.["setup-welcomeSubtitle"] ?? "Let's get you set up with your perfect configuration."} {t()?.["setup-welcomeSubtitle"] ?? "Let's get you set up with your perfect configuration."}
</p> </p>
<Button <Motion.button
color={ButtonColors.PRIMARY}
size={ButtonSizes.LARGE}
onClick={onNext} onClick={onNext}
class="setup-cta" class="px-6 py-2.5 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-medium"
style={ctaStyle}
> >
{t()?.["setup-getStarted"] ?? "Get Started"} {t()?.["setup-getStarted"] ?? "Get Started"}
</Button> </Motion.button>
</Motion.div> </Motion.div>
); );
const WindowStyle = ({ const WindowStyle = ({
selectedStyle, readyToNext,
onSelect,
t, t,
}: { }: {
selectedStyle: Accessor<string>; readyToNext: (valid: boolean) => void;
onSelect: (styleId: string) => void; t: () => Record<string, string> | undefined;
t: () => Lang;
}) => { }) => {
const [selectedStyle, setSelectedStyle] = createSignal<string | null>(null);
const handleStyleSelect = (styleId: string) => {
const newValue = selectedStyle() === styleId ? null : styleId;
setSelectedStyle(newValue);
if (newValue === null) {
readyToNext(false);
} else {
readyToNext(true);
window.setup.saveSettings({ windowStyle: newValue });
}
};
const styles = [ const styles = [
{
id: "overlay",
titleKey: "setup-windowStyle-overlayTitle",
titleFallback: "Overlay Titlebar",
descKey: "setup-windowStyle-overlayDesc",
descFallback: "A modern titlebar that blends into Discord. Recommended for most users.",
screenshot: "legcord://assets/overlay.png",
recommended: true,
},
{ {
id: "native", id: "native",
titleKey: "setup-windowStyle-nativeTitle", titleKey: "setup-windowStyle-nativeTitle",
titleFallback: "Native Window",
descKey: "setup-windowStyle-nativeDesc", descKey: "setup-windowStyle-nativeDesc",
descFallback: "Use your system's default window decorations",
screenshot: "legcord://assets/native.png", screenshot: "legcord://assets/native.png",
}, },
{ {
id: "default", id: "default",
titleKey: "setup-windowStyle-customTitle", titleKey: "setup-windowStyle-customTitle",
titleFallback: "Custom Titlebar",
descKey: "setup-windowStyle-customDesc", descKey: "setup-windowStyle-customDesc",
descFallback: "Use Legcord's custom titlebar design",
screenshot: "legcord://assets/custom.png", screenshot: "legcord://assets/custom.png",
}, },
]; ];
const lang = t(); const lang = t();
return ( return (
<div class="setup-step"> <div class="space-y-6">
<div class="setup-header"> <div class="text-center">
<h2 class="setup-title">{lang?.["setup-chooseWindowStyle"] ?? "Choose Window Style"}</h2> <Monitor class="w-8 h-8 text-purple-400 mx-auto mb-4" />
<p class="setup-subtitle"> <h2 class="text-2xl font-bold text-white mb-2">
{lang?.["setup-chooseWindowStyle"] ?? "Choose Window Style"}
</h2>
<p class="text-gray-400">
{lang?.["setup-selectAppearance"] ?? "Select how Legcord appears on your machine"} {lang?.["setup-selectAppearance"] ?? "Select how Legcord appears on your machine"}
</p> </p>
</div> </div>
<div class="setup-options"> <div class="space-y-4">
<For each={styles}> <For each={styles}>
{(item) => ( {(item) => (
<OptionCard <Motion.button
selected={selectedStyle() === item.id} onClick={() => handleStyleSelect(item.id)}
onSelect={() => onSelect(item.id)} class={`group relative w-full p-4 rounded-xl transition-all duration-300 text-left ${
title={lang?.[item.titleKey] ?? item.titleFallback} selectedStyle() === item.id
description={lang?.[item.descKey] ?? item.descFallback} ? "bg-purple-900/40 border border-purple-500/50 shadow-lg shadow-purple-500/20"
badge={item.recommended ? (lang?.["setup-recommended"] ?? "Recommended") : undefined} : "bg-gray-800/40 border border-gray-700/30 hover:bg-gray-800/60"
preview={item.screenshot} }`}
/> >
<div class="flex items-center gap-4">
<div class="w-32 h-24 rounded-lg overflow-hidden flex-shrink-0">
<img
src={item.screenshot}
alt={`${lang?.[item.titleKey] ?? item.titleKey} preview`}
class="w-full h-full object-cover"
/>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white">
{lang?.[item.titleKey] ?? item.titleKey}
</h3>
<p class="text-gray-400 text-sm mt-1">{lang?.[item.descKey] ?? item.descKey}</p>
</div>
<div
class={`w-5 h-5 rounded-full border-2 transition-colors ${
selectedStyle() === item.id ? "border-purple-400" : "border-gray-600"
}`}
>
{selectedStyle() === item.id && (
<div class="w-3 h-3 m-0.5 rounded-full bg-purple-400" />
)}
</div>
</div>
</Motion.button>
)} )}
</For> </For>
</div> </div>
@ -170,166 +125,224 @@ const WindowStyle = ({
}; };
const TraySettings = ({ const TraySettings = ({
selectedOption, readyToNext,
onSelect,
t, t,
}: { }: {
selectedOption: Accessor<string | null>; readyToNext: (valid: boolean) => void;
onSelect: (optionId: string) => void; t: () => Record<string, string> | undefined;
t: () => Lang;
}) => { }) => {
const [selectedOption, setSelectedOption] = createSignal<string | null>(null);
const handleOptionSelect = (optionId: string) => {
const newValue = selectedOption() === optionId ? null : optionId;
setSelectedOption(newValue);
if (newValue === null) {
readyToNext(false);
} else {
readyToNext(true);
window.setup.saveSettings({ tray: newValue });
}
};
const options = [ const options = [
{ { id: "dynamic", titleKey: "setup-trayEnableTitle", descKey: "setup-trayEnableDesc", icon: LaptopMinimalCheck },
id: "dynamic", { id: "disabled", titleKey: "setup-trayDisableTitle", descKey: "setup-trayDisableDesc", icon: LaptopMinimal },
titleKey: "setup-trayEnableTitle",
titleFallback: "Enable Tray Icon",
descKey: "setup-trayEnableDesc",
descFallback: "Show Legcord in your system tray",
},
{
id: "disabled",
titleKey: "setup-trayDisableTitle",
titleFallback: "Disable Tray Icon",
descKey: "setup-trayDisableDesc",
descFallback: "Don't show Legcord in your system tray",
},
]; ];
const lang = t(); const lang = t();
return ( return (
<div class="setup-step"> <div class="space-y-6">
<div class="setup-header"> <div class="text-center">
<h2 class="setup-title">{lang?.["setup-systemTray"] ?? "System Tray"}</h2> <MinimizeIcon class="w-8 h-8 text-purple-400 mx-auto mb-4" />
<p class="setup-subtitle"> <h2 class="text-2xl font-bold text-white mb-2">{lang?.["setup-systemTray"] ?? "System Tray"}</h2>
<p class="text-gray-400">
{lang?.["setup-trayChoose"] ?? "Choose whether to enable the system tray icon"} {lang?.["setup-trayChoose"] ?? "Choose whether to enable the system tray icon"}
</p> </p>
</div> </div>
<Show when={window.setup.os === "linux"}> <Show when={window.setup.os === "linux"}>
<div class="setup-warning"> <div class="space-y-4">
<WarningIcon /> <div class="p-4 rounded-xl bg-yellow-500/10 border border-yellow-500/20 flex items-center gap-3">
<span> <CircleAlert class="w-5 h-5 text-yellow-400 flex-shrink-0" />
{lang?.["setup-linuxTrayWarning"] ?? <div>
"System tray functionality may have issues or behave differently on Linux systems."} <p class="text-yellow-200/80 text-sm mt-0.5">
</span> {lang?.["setup-linuxTrayWarning"] ??
"System tray functionality may have issues or behave differently on Linux systems."}
</p>
</div>
</div>
</div> </div>
</Show> </Show>
<For each={options}>
<div class="setup-options"> {(option) => (
<For each={options}> <Motion.button
{(option) => ( onClick={() => handleOptionSelect(option.id)}
<OptionCard class={`group relative w-full p-4 rounded-xl transition-all duration-300 text-left ${
selected={selectedOption() === option.id} selectedOption() === option.id
onSelect={() => onSelect(option.id)} ? "bg-purple-900/40 border border-purple-500/50 shadow-lg shadow-purple-500/20"
title={lang?.[option.titleKey] ?? option.titleFallback} : "bg-gray-800/40 border border-gray-700/30 hover:bg-gray-800/60"
description={lang?.[option.descKey] ?? option.descFallback} }`}
/> >
)} <div class="flex items-start gap-4">
</For> <div
</div> class={`w-10 h-10 rounded-lg transition-colors flex items-center justify-center ${
selectedOption() === option.id ? "bg-purple-500/20" : "bg-gray-800/40"
}`}
>
<option.icon class="w-6 h-6 text-purple-400" />
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white">
{lang?.[option.titleKey] ?? option.titleKey}
</h3>
<p class="text-gray-400 text-sm mt-1">{lang?.[option.descKey] ?? option.descKey}</p>
</div>
<div
class={`w-5 h-5 rounded-full border-2 transition-colors ${
selectedOption() === option.id ? "border-purple-400" : "border-gray-600"
}`}
>
{selectedOption() === option.id && (
<div class="w-3 h-3 m-0.5 rounded-full bg-purple-400" />
)}
</div>
</div>
</Motion.button>
)}
</For>
</div> </div>
); );
}; };
const Finish = ({ restart, t }: { restart: () => void; t: () => Lang }) => ( const Finish = ({ restart, t }: { restart: () => void; t: () => Record<string, string> | undefined }) => (
<Motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} class="setup-finish"> <div class="text-center">
<div class="setup-finish-icon" aria-hidden="true"> <Box class="w-8 h-8 text-purple-400 mx-auto mb-4" />
<svg viewBox="0 0 24 24" fill="none" role="presentation"> <h2 class="text-2xl font-bold text-white mb-4">{t()?.["setup-finishTitle"] ?? "You're All Set!"}</h2>
<circle cx="12" cy="12" r="12" fill="var(--brand-500)" /> <p class="text-gray-400 mb-6">
<path
d="M7.5 12.5 10.5 15.5 16.5 8.5"
stroke="white"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<h1 class="setup-title setup-title--hero">{t()?.["setup-finishTitle"] ?? "You're All Set!"}</h1>
<p class="setup-subtitle setup-subtitle--hero">
{t()?.["setup-finishSubtitle"] ?? {t()?.["setup-finishSubtitle"] ??
"Your Legcord configuration is complete and personalized to your preferences."} "Your Legcord configuration is complete and personalized to your preferences."}
</p> </p>
<div class="setup-note"> <div class="p-4 rounded-xl bg-gray-800/40 border border-gray-700/30 mb-6">
<p> <Settings class="w-6 h-6 text-purple-400 mx-auto mb-2" />
<p class="text-sm text-gray-400">
{t()?.["setup-finishSettingsNote"] ?? {t()?.["setup-finishSettingsNote"] ??
"Need to make changes later? You'll find all these options in Discord's settings menu under Legcord."} "Need to make changes later? You'll find all these options in Discord's settings menu under Legcord."}
</p> </p>
</div> </div>
<Button <Motion.button
color={ButtonColors.PRIMARY}
size={ButtonSizes.LARGE}
onClick={restart} onClick={restart}
class="setup-cta" class="px-6 py-2.5 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-medium"
style={ctaStyle}
> >
{t()?.["setup-launchLegcord"] ?? "Launch Legcord"} {t()?.["setup-launchLegcord"] ?? "Launch Legcord"}
</Button> </Motion.button>
</Motion.div> </div>
); );
const ModSelector = ({ const ModSelector = ({
selectedMod, readyToNext,
onSelect,
t, t,
}: { }: {
selectedMod: Accessor<string>; readyToNext: (valid: boolean) => void;
onSelect: (modId: string) => void; t: () => Record<string, string> | undefined;
t: () => Lang;
}) => { }) => {
const [selectedMod, setSelectedMod] = createSignal<string | null>(null);
const handleModSelect = (optionId: string) => {
const newValue = selectedMod() === optionId ? null : optionId;
setSelectedMod(newValue);
if (newValue === null) {
readyToNext(false);
} else {
readyToNext(true);
if (newValue !== "shelter") {
window.setup.saveSettings({ mods: [newValue] });
} else {
window.setup.saveSettings({ mods: [] });
}
}
};
const mods = [ const mods = [
{
id: "shelter",
titleKey: "setup-shelterOnlyTitle",
titleFallback: "Shelter Only",
descKey: "setup-shelterOnlyDesc",
descFallback: "Legcord was built around Shelter. Most typical users won't need another client mod.",
recommended: true,
icon: "legcord://assets/shelter.svg",
},
{ {
id: "vencord", id: "vencord",
titleKey: "setup-vencordTitle", titleKey: "setup-vencordTitle",
titleFallback: "Vencord",
descKey: "setup-vencordDesc", descKey: "setup-vencordDesc",
descFallback: "Client mod with plugins and themes.",
icon: "legcord://assets/vencord.png", icon: "legcord://assets/vencord.png",
}, },
{ {
id: "equicord", id: "equicord",
titleKey: "setup-equicordTitle", titleKey: "setup-equicordTitle",
titleFallback: "Equicord",
descKey: "setup-equicordDesc", descKey: "setup-equicordDesc",
descFallback: "A fork of Vencord with more plugins.",
icon: "legcord://assets/equicord.png", icon: "legcord://assets/equicord.png",
}, },
]; ];
const lang = t(); const lang = t();
return ( return (
<div class="setup-step"> <div class="space-y-6">
<div class="setup-header"> <div class="text-center">
<h2 class="setup-title">{lang?.["setup-modSelectorTitle"] ?? "Choose Your Client Mod"}</h2> <PuzzleIcon class="w-8 h-8 text-purple-400 mx-auto mb-4" />
<p class="setup-subtitle"> <h2 class="text-2xl font-bold text-white mb-2">
{lang?.["setup-modSelectorTitle"] ?? "Choose Your Client Mod"}
</h2>
<p class="text-gray-400">
{lang?.["setup-modSelectorSubtitle"] ?? {lang?.["setup-modSelectorSubtitle"] ??
"Legcord includes Shelter out of the box, but you can also choose another client mod if wanted."} "Legcord includes Shelter out of the box, but you can also choose another client mod if wanted."}
</p> </p>
</div> </div>
<div class="setup-options"> <div class="space-y-3">
<For each={mods}> <For each={mods} fallback={<div>{lang?.["setup-loading"] ?? "Loading..."}</div>}>
{(mod) => ( {(mod) => (
<OptionCard <Motion.button
selected={selectedMod() === mod.id} onClick={() => handleModSelect(mod.id)}
onSelect={() => onSelect(mod.id)} class={`group relative w-full p-4 rounded-xl transition-all duration-300 text-left ${
title={lang?.[mod.titleKey] ?? mod.titleFallback} selectedMod() === mod.id
description={lang?.[mod.descKey] ?? mod.descFallback} ? "bg-purple-900/40 border border-purple-500/50 shadow-lg shadow-purple-500/20"
badge={mod.recommended ? (lang?.["setup-recommended"] ?? "Recommended") : undefined} : "bg-gray-800/40 border border-gray-700/30 hover:bg-gray-800/60"
icon={mod.icon} }`}
/> >
<div class="flex items-start gap-4">
<div
class={`w-10 h-10 rounded-lg transition-colors overflow-hidden ${
selectedMod() === mod.id ? "bg-purple-500/20" : "bg-gray-800/40"
}`}
>
<img
src={mod.icon}
alt={`${lang?.[mod.titleKey] ?? mod.titleKey} icon`}
class="w-full h-full object-contain p-1.5"
/>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white">
{lang?.[mod.titleKey] ?? mod.titleKey}
</h3>
<p class="text-gray-400 text-sm mt-1">{lang?.[mod.descKey] ?? mod.descKey}</p>
</div>
<div
class={`w-5 h-5 rounded-full border-2 transition-colors ${
selectedMod() === mod.id ? "border-purple-400" : "border-gray-600"
}`}
>
{selectedMod() === mod.id && (
<div class="w-3 h-3 m-0.5 rounded-full bg-purple-400" />
)}
</div>
</div>
</Motion.button>
)} )}
</For> </For>
</div> </div>
<Motion.button
onClick={() => handleModSelect("shelter")}
class={`w-full px-6 py-2.5 rounded-xl border transition-colors font-medium
${
selectedMod() === "shelter"
? "border-purple-500/50 text-purple-400 hover:bg-purple-500/10"
: "border-gray-700/30 text-gray-500 hover:border-purple-500/50 hover:text-purple-400 hover:bg-purple-500/10"
}`}
>
{lang?.["setup-useShelterOnly"] ?? "Use Shelter Only"}
</Motion.button>
</div> </div>
); );
}; };
@ -337,117 +350,84 @@ const ModSelector = ({
function Stepper() { function Stepper() {
const [t] = createResource(() => window.setup.getRawLang()); const [t] = createResource(() => window.setup.getRawLang());
const [currentStep, setCurrentStep] = createSignal(0); const [currentStep, setCurrentStep] = createSignal(0);
const [selectedStyle, setSelectedStyle] = createSignal("overlay"); const [isValid, setValid] = createSignal(true);
const [selectedMod, setSelectedMod] = createSignal("shelter");
const [selectedTray, setSelectedTray] = createSignal<string | null>(null);
const maxSteps = 5; const maxSteps = 5;
onMount(() => {
window.setup.saveSettings({ windowStyle: "overlay", mods: [] });
});
const canProceed = createMemo(() => {
switch (currentStep()) {
case 0:
case 1:
case 2:
return true;
case 3:
return selectedTray() !== null;
default:
return true;
}
});
const handleStyleSelect = (styleId: string) => {
setSelectedStyle(styleId);
window.setup.saveSettings({ windowStyle: styleId });
};
const handleModSelect = (modId: string) => {
setSelectedMod(modId);
if (modId !== "shelter") {
window.setup.saveSettings({ mods: [modId] });
} else {
window.setup.saveSettings({ mods: [] });
}
};
const handleTraySelect = (trayId: string) => {
setSelectedTray(trayId);
window.setup.saveSettings({ tray: trayId });
};
const handleNext = () => { const handleNext = () => {
if (!canProceed()) return; if (!isValid()) return;
setCurrentStep((prev) => Math.min(prev + 1, maxSteps - 1)); setCurrentStep((prev) => prev + 1);
setValid(false);
}; };
const handleBack = () => { const handleBack = () => {
setCurrentStep((prev) => Math.max(prev - 1, 0)); setCurrentStep((prev) => prev - 1);
if (currentStep() === 0) {
setValid(true);
}
};
const setReady = (valid: boolean) => {
setValid(valid);
}; };
const restart = () => { const restart = () => {
console.log("Restarting..."); console.log("Restarting...");
window.setup.saveSettings({ doneSetup: true }); window.setup.saveSettings({ doneSetup: true });
window.setup.restart(); window.setup.restart();
}; };
const stepOfText = () => { const stepOfText = () => {
const lang = t(); const lang = t();
const template = lang?.["setup-stepOf"] ?? "Step {current} of {total}"; const template = lang?.["setup-stepOf"] ?? "Step {current} of {total}";
return template.replace("{current}", String(currentStep() + 1)).replace("{total}", String(maxSteps)); return template.replace("{current}", String(currentStep() + 1)).replace("{total}", String(maxSteps));
}; };
return ( return (
<Motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} class="setup-shell"> <div class="min-h-screen flex items-center justify-center">
<Show when={currentStep() !== 0 && currentStep() !== 4}> <Motion.div
<Progress current={currentStep()} total={maxSteps} /> initial={{ opacity: 0, y: 20 }}
</Show> animate={{ opacity: 1, y: 0 }}
class="w-full max-w-md p-8 rounded-3xl bg-gradient-to-br from-[#171631] via-gray-900 to-[#171631] backdrop-blur-xl border border-gray-800/50 shadow-2xl"
<div class={`setup-body ${niceScrollbarsClass()}`}> >
<Show when={currentStep() === 0}> <Show when={currentStep() === 0}>
<Welcome onNext={handleNext} t={t} /> <Welcome onNext={handleNext} t={t} />
</Show> </Show>
<Show when={currentStep() === 1}> <Show when={currentStep() === 1}>
<WindowStyle selectedStyle={selectedStyle} onSelect={handleStyleSelect} t={t} /> <WindowStyle readyToNext={setReady} t={t} />
</Show> </Show>
<Show when={currentStep() === 2}> <Show when={currentStep() === 2}>
<ModSelector selectedMod={selectedMod} onSelect={handleModSelect} t={t} /> <ModSelector readyToNext={setReady} t={t} />
</Show> </Show>
<Show when={currentStep() === 3}> <Show when={currentStep() === 3}>
<TraySettings selectedOption={selectedTray} onSelect={handleTraySelect} t={t} /> <TraySettings readyToNext={setReady} t={t} />
</Show> </Show>
<Show when={currentStep() === 4}> <Show when={currentStep() === 4}>
<Finish restart={restart} t={t} /> <Finish restart={restart} t={t} />
</Show> </Show>
</div>
<Show when={currentStep() !== 0 && currentStep() !== 4}> <Show when={currentStep() !== 0 && currentStep() !== 4}>
<div class="setup-footer"> <div class="mt-8 flex items-center justify-between">
<Button <Motion.button
color={ButtonColors.SECONDARY} onClick={handleBack}
size={ButtonSizes.MEDIUM} class="px-6 py-2.5 rounded-xl bg-gray-800 hover:bg-gray-700 text-white font-medium transition-colors inline-flex items-center gap-2"
onClick={handleBack} >
class="setup-footer-btn" <ChevronLeft class="w-4 h-4" />
style={footerBtnStyle} {t()?.["setup-back"] ?? "Back"}
> </Motion.button>
{t()?.["setup-back"] ?? "Back"}
</Button> <div class="px-4 py-1 rounded-full bg-gray-800/50 text-gray-400 font-medium text-sm">
<span class="setup-step-indicator">{stepOfText()}</span> {stepOfText()}
<Button </div>
color={ButtonColors.PRIMARY}
size={ButtonSizes.MEDIUM} <Motion.button
disabled={!canProceed()} onClick={handleNext}
onClick={handleNext} class={`px-6 py-2.5 rounded-xl text-white font-medium transition-colors inline-flex items-center gap-2 ${
class="setup-footer-btn" isValid()
style={footerBtnStyle} ? "bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500"
> : "bg-gray-700 cursor-not-allowed opacity-50"
{t()?.["setup-next"] ?? "Next"} }`}
</Button> >
</div> {t()?.["setup-next"] ?? "Next"}
</Show> <ChevronRight class="w-4 h-4" />
</Motion.div> </Motion.button>
</div>
</Show>
</Motion.div>
</div>
); );
} }

View file

@ -142,7 +142,6 @@ export function SettingsPage() {
t["settings-venmic-workaround"], t["settings-venmic-workaround"],
t["settings-audio"], t["settings-audio"],
t["settings-hardwareAcceleration"], t["settings-hardwareAcceleration"],
t["settings-sdpH264BaselineRewrite"],
t["settings-vaapi"], t["settings-vaapi"],
t["settings-automaticClientUpdates"], t["settings-automaticClientUpdates"],
t["settings-disableHttpCache"], t["settings-disableHttpCache"],
@ -489,7 +488,7 @@ export function SettingsPage() {
<SwitchItem <SwitchItem
note={t["settings-startMinimized-desc"]} note={t["settings-startMinimized-desc"]}
value={settings.startMinimized} value={settings.startMinimized}
onChange={(e: boolean) => setConfig("startMinimized", e, true)} onChange={(e: boolean) => setConfig("startMinimized", e)}
> >
{t["settings-startMinimized"]} {t["settings-startMinimized"]}
</SwitchItem> </SwitchItem>
@ -857,26 +856,6 @@ export function SettingsPage() {
{t["settings-hardwareAcceleration"]} {t["settings-hardwareAcceleration"]}
</SwitchItem> </SwitchItem>
</SearchableSetting> </SearchableSetting>
<Show when={window.legcord.platform === "darwin" || window.legcord.platform === "win32"}>
<SearchableSetting
keywords={[
t["settings-sdpH264BaselineRewrite"],
t["settings-sdpH264BaselineRewrite-desc"],
"H264",
"SDP",
"VideoToolbox",
"OpenH264",
]}
>
<SwitchItem
note={t["settings-sdpH264BaselineRewrite-desc"]}
value={settings.sdpH264BaselineRewrite ?? true}
onChange={(e: boolean) => setConfig("sdpH264BaselineRewrite", e, true)}
>
{t["settings-sdpH264BaselineRewrite"]}
</SwitchItem>
</SearchableSetting>
</Show>
<Show when={window.legcord.platform === "linux"}> <Show when={window.legcord.platform === "linux"}>
<SearchableSetting keywords={[t["settings-vaapi"], t["settings-vaapi-desc"], "VAAPI"]}> <SearchableSetting keywords={[t["settings-vaapi"], t["settings-vaapi-desc"], "VAAPI"]}>
<SwitchItem <SwitchItem

View file

@ -1,12 +1,12 @@
import path from "node:path"; import path from "node:path";
import { BrowserWindow, ipcMain } from "electron"; import { BrowserWindow, ipcMain } from "electron";
import isDev from "electron-is-dev"; import isDev from "electron-is-dev";
import { getConfig, shouldStartMinimized } from "../common/config.js"; import { getConfig } from "../common/config.js";
import { getLang } from "../common/lang.js"; import { getLang } from "../common/lang.js";
export let splashWindow: BrowserWindow; export let splashWindow: BrowserWindow;
export async function createSplashWindow(): Promise<void> { export async function createSplashWindow(): Promise<void> {
if (shouldStartMinimized()) return; if (getConfig("startMinimized")) return;
splashWindow = new BrowserWindow({ splashWindow = new BrowserWindow({
width: 300, width: 300,
height: 350, height: 350,