mirror of
https://github.com/smartfrigde/armcord.git
synced 2026-08-19 06:03:22 +00:00
Compare commits
6 commits
b2151296a4
...
e9a95d4c75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a95d4c75 | ||
|
|
c222657c30 | ||
|
|
9558af52dc | ||
|
|
acdec4a5e3 | ||
|
|
e60e46c88e | ||
|
|
71c6484a74 |
27 changed files with 1512 additions and 453 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,4 +5,5 @@ ts-out/
|
|||
package-lock.json
|
||||
.pnpm-store
|
||||
.cache
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
scripts/spdx-license.txt
|
||||
BIN
assets/app/desktop.png
Normal file
BIN
assets/app/desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/app/overlay.png
Normal file
BIN
assets/app/overlay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
1
assets/app/shelter.svg
Normal file
1
assets/app/shelter.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="322" height="322" fill="none" viewBox="0 0 322 322"><g clip-path="url(#clip0_77_26)"><path fill="url(#paint0_linear_77_26)" d="M297 0H25C11.1929 0 0 11.1929 0 25V297C0 310.807 11.1929 322 25 322H297C310.807 322 322 310.807 322 297V25C322 11.1929 310.807 0 297 0Z"/><g filter="url(#filter0_d_77_26)"><path fill="#CCFFED" d="M99.4277 199.184C103.018 227.904 130.909 239.778 161.286 239.778C197.186 239.778 221.763 224.037 221.763 193.661C221.763 175.158 209.889 159.97 185.587 154.723L152.725 147.543C141.955 145.058 140.298 140.363 140.298 135.945C140.298 129.593 144.441 122.413 159.077 122.413C174.265 122.413 183.93 130.422 185.311 141.468L220.106 134.564C215.412 110.539 192.767 93.6935 160.181 93.6935C125.386 93.6935 103.846 112.748 103.846 139.259C103.846 159.142 117.93 173.225 142.784 178.472L170.951 184.271C182.826 186.757 186.14 191.728 186.14 196.698C186.14 204.431 178.131 210.506 164.6 210.506C150.24 210.506 138.089 205.811 136.156 191.728L99.4277 199.184Z"/></g></g><defs><filter id="filter0_d_77_26" width="122.335" height="153.085" x="99.428" y="93.694" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="7"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow_77_26"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_77_26" mode="normal" result="shape"/></filter><linearGradient id="paint0_linear_77_26" x1="161" x2="161" y1="0" y2="1105" gradientUnits="userSpaceOnUse"><stop stop-color="#2A3B4B"/><stop offset="1" stop-color="#2BFAAC"/></linearGradient><clipPath id="clip0_77_26"><rect width="322" height="322" fill="#fff"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -78,6 +78,8 @@
|
|||
"settings-automaticClientUpdates-desc": "Disables automatic Legcord updates",
|
||||
"settings-hardwareAcceleration": "Hardware acceleration",
|
||||
"settings-hardwareAcceleration-desc": "Hardware acceleration uses your GPU to make Legcord run faster. If you're experiencing visual glitches, try disabling this.",
|
||||
"settings-sdpH264BaselineRewrite": "H.264 Baseline SDP rewrite",
|
||||
"settings-sdpH264BaselineRewrite-desc": "Rewrites Discord's Constrained Baseline H.264 profile in WebRTC SDP so screenshare can use hardware encode (VideoToolbox / Media Foundation) instead of OpenH264. Disable if Go Live fails to connect or video is broken, then restart Legcord.",
|
||||
"settings-processScanning": "Process scanning",
|
||||
"settings-processScanning-desc": "Scan for running games to improve Rich Presence detection. Disable to reduce CPU usage. Requires a restart.",
|
||||
"settings-windowsLegacyScanning": "Windows legacy scanning",
|
||||
|
|
@ -244,10 +246,13 @@
|
|||
"setup-welcomeTitle": "Welcome to Legcord",
|
||||
"setup-welcomeSubtitle": "Let's get you set up with your perfect configuration.",
|
||||
"setup-getStarted": "Get Started",
|
||||
"setup-windowStyle-overlayTitle": "Overlay Titlebar",
|
||||
"setup-windowStyle-overlayDesc": "A modern titlebar that blends into Discord. Recommended for most users.",
|
||||
"setup-windowStyle-nativeTitle": "Native Window",
|
||||
"setup-windowStyle-nativeDesc": "Use your system's default window decorations",
|
||||
"setup-windowStyle-customTitle": "Custom Titlebar",
|
||||
"setup-windowStyle-customDesc": "Use Legcord's custom titlebar design",
|
||||
"setup-recommended": "Recommended",
|
||||
"setup-chooseWindowStyle": "Choose Window Style",
|
||||
"setup-selectAppearance": "Select how Legcord appears on your machine",
|
||||
"setup-systemTray": "System Tray",
|
||||
|
|
@ -263,6 +268,8 @@
|
|||
"setup-launchLegcord": "Launch Legcord",
|
||||
"setup-modSelectorTitle": "Choose Your Client Mod",
|
||||
"setup-modSelectorSubtitle": "Legcord includes Shelter out of the box, but you can also choose another client mod if wanted.",
|
||||
"setup-shelterOnlyTitle": "Shelter Only",
|
||||
"setup-shelterOnlyDesc": "Legcord was built around Shelter. Most typical users won't need another client mod.",
|
||||
"setup-vencordTitle": "Vencord",
|
||||
"setup-vencordDesc": "Client mod with plugins and themes.",
|
||||
"setup-equicordTitle": "Equicord",
|
||||
|
|
|
|||
21
docker/Dockerfile.deb
Normal file
21
docker/Dockerfile.deb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM ubuntu
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./dist/*.deb .
|
||||
|
||||
RUN apt-get update -y && apt-get upgrade -y
|
||||
|
||||
RUN apt-get install ./*.deb -y
|
||||
|
||||
RUN apt-get install -qqy x11-apps x11vnc xvfb
|
||||
|
||||
RUN mkdir ~/.vnc
|
||||
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
|
||||
|
||||
COPY docker/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 5900
|
||||
|
||||
CMD ["/start.sh"]
|
||||
21
docker/Dockerfile.pacman
Normal file
21
docker/Dockerfile.pacman
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM archlinux
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./dist/*.pacman .
|
||||
|
||||
RUN pacman -Syu --noconfirm
|
||||
|
||||
RUN pacman -U --noconfirm ./*.pacman
|
||||
|
||||
RUN pacman -Syu --noconfirm x11vnc xorg-server-xvfb
|
||||
|
||||
RUN mkdir ~/.vnc
|
||||
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
|
||||
|
||||
COPY docker/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 5900
|
||||
|
||||
CMD ["/start.sh"]
|
||||
13
docker/start.sh
Normal file
13
docker/start.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export DISPLAY=:99
|
||||
|
||||
Xvfb :99 -screen 0 1280x800x24 &
|
||||
sleep 1
|
||||
|
||||
x11vnc -display :99 -forever -usepw -rfbport 5900 &
|
||||
|
||||
legcord --no-sandbox &
|
||||
|
||||
tail -f /dev/null
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
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") =>
|
||||
|
|
@ -71,7 +72,7 @@ export const config: Configuration = {
|
|||
},
|
||||
|
||||
pacman: {
|
||||
depends: ["gtk3", "libnotify", "nss", "libxss", "libxtst", "xdg-utils", "at-spi2-core", "libsecret"],
|
||||
depends: ["gtk3", "libnotify", "xdg-utils", "at-spi2-core", "alsa-lib", "nspr", "nss"],
|
||||
},
|
||||
|
||||
nsis: {
|
||||
|
|
@ -101,13 +102,14 @@ export const config: Configuration = {
|
|||
deb: {
|
||||
category: "Network",
|
||||
icon: "build/icon.icns",
|
||||
depends: ["libgbm-dev", "libasound2", "libnspr4", "libnss3"],
|
||||
depends: ["libasound2", "libnspr4", "libnss3", "libasound2t64", "libasound2-plugins"],
|
||||
desktop: {
|
||||
entry: {
|
||||
Actions: availableActions,
|
||||
},
|
||||
desktopActions: desktopActions("/opt/Legcord/legcord"),
|
||||
},
|
||||
fpm: [`${debianLicence()}=/usr/share/doc/legcord/copyright`],
|
||||
},
|
||||
|
||||
files: [
|
||||
|
|
|
|||
|
|
@ -40,17 +40,19 @@
|
|||
"@types/ws": "^8.18.1",
|
||||
"@uwu/lune": "^1.6.2",
|
||||
"@uwu/shelter-defs": "^1.5.1",
|
||||
"@uwu/shelter-ui": "^0.0.6",
|
||||
"@xmldom/xmldom": "^0.9.10",
|
||||
"app-builder-lib": "^26.8.1",
|
||||
"babel-preset-solid": "^1.9.12",
|
||||
"electron": "43.2.0",
|
||||
"electron-builder": "26.8.1",
|
||||
"electron-builder": "26.15.7",
|
||||
"lucide-solid": "^1.16.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
"rolldown": "1.0.3",
|
||||
"rolldown": "1.2.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"solid-js": "^1.9.14",
|
||||
"solid-motionone": "^1.0.4",
|
||||
"spdx-license-list": "^6.11.0",
|
||||
"typescript": "^6.0.3",
|
||||
"xml-formatter": "^3.7.0"
|
||||
},
|
||||
|
|
|
|||
565
pnpm-lock.yaml
generated
565
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -31,6 +31,8 @@ export default defineConfig([
|
|||
targets: [
|
||||
{ src: "src/**/**/*.html", dest: "ts-out/html/" },
|
||||
{ src: "src/**/**/*.css", dest: "ts-out/css/" },
|
||||
{ src: "src/setup/setup.css", dest: "ts-out/html/" },
|
||||
{ src: "node_modules/@uwu/shelter-ui/compat.css", dest: "ts-out/html/" },
|
||||
{ src: "src/**/**/*.js", dest: "ts-out/js/" },
|
||||
{ src: "package.json", dest: "ts-out/" },
|
||||
{ src: "assets/**/**", dest: "ts-out/assets/" },
|
||||
|
|
|
|||
66
scripts/spdxLicenceDebianFormat.ts
Normal file
66
scripts/spdxLicenceDebianFormat.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// https://www.debian.org/doc/debian-policy/ch-docs.html#copyright-information
|
||||
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import spdxLicenseList from "spdx-license-list";
|
||||
|
||||
interface CopyrightConfig {
|
||||
upstreamName: string;
|
||||
source: string;
|
||||
copyrightHolder: string;
|
||||
licenseId: string;
|
||||
comment: string; // optional but nice to have
|
||||
}
|
||||
|
||||
function indentText(text: string): string {
|
||||
// formatted text fields follow the same rules as Debian control file
|
||||
// long descriptions: each line indented with a single space, blank
|
||||
// lines represented as " ."
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => (line.trim() === "" ? " ." : ` ${line}`))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function generateCopyrightFile(config: CopyrightConfig): string {
|
||||
const license = spdxLicenseList[config.licenseId];
|
||||
if (!license) {
|
||||
throw new Error(`Unknown SPDX license id: ${config.licenseId}`);
|
||||
}
|
||||
|
||||
const header = [
|
||||
"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/",
|
||||
`Upstream-Name: ${config.upstreamName}`,
|
||||
`Source: ${config.source}`,
|
||||
`Comment: ${indentText(config.comment)}`, // optional field
|
||||
].join("\n");
|
||||
|
||||
const licenseText = readFileSync("license.txt").toString();
|
||||
|
||||
const licenseStanza = [`License: ${config.licenseId}`, indentText(licenseText)].join("\n");
|
||||
|
||||
const filesStanza = ["Files: *", `Copyright: ${config.copyrightHolder}`, `License: ${config.licenseId}`].join("\n");
|
||||
|
||||
return `${[header, filesStanza, licenseStanza].join("\n\n")}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Debian SPDX-formatted license for given license ID
|
||||
*
|
||||
* @export
|
||||
* @return {*} {string}
|
||||
*/
|
||||
export default function (): string {
|
||||
const copyrightContent: string = generateCopyrightFile({
|
||||
upstreamName: "Legcord",
|
||||
source: "https://github.com/Legcord/Legcord",
|
||||
copyrightHolder: `2020 - ${new Date().getFullYear()} Legcord Contributors`, // git log --reverse
|
||||
licenseId: "OSL-3.0",
|
||||
comment: "Open-Source Discord client alternative.",
|
||||
});
|
||||
|
||||
const licensePath = path.join(__dirname, "spdx-license.txt");
|
||||
writeFileSync(licensePath, copyrightContent);
|
||||
return licensePath;
|
||||
}
|
||||
2
src/@types/settings.d.ts
vendored
2
src/@types/settings.d.ts
vendored
|
|
@ -61,6 +61,8 @@ export interface Settings {
|
|||
startMinimized: boolean;
|
||||
keybinds: Keybind[];
|
||||
hardwareAcceleration: boolean;
|
||||
/** Rewrite H.264 Constrained Baseline (42e0) → Baseline (4200) in WebRTC SDP for HW encode. */
|
||||
sdpH264BaselineRewrite: boolean;
|
||||
useMacSystemPicker: boolean;
|
||||
inviteWebsocket: boolean;
|
||||
disableAutogain: boolean;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ let configCache: Settings | null = null;
|
|||
let configCacheTime = 0;
|
||||
const CONFIG_CACHE_TTL = 5000; // Cache for 5 seconds
|
||||
const defaults: Settings = {
|
||||
windowStyle: "default",
|
||||
windowStyle: "overlay",
|
||||
channel: "stable",
|
||||
bounceOnPing: false,
|
||||
csp: "none",
|
||||
|
|
@ -34,11 +34,12 @@ const defaults: Settings = {
|
|||
loopbackType: "loopback",
|
||||
},
|
||||
multiInstance: false,
|
||||
mods: ["vencord"],
|
||||
mods: [],
|
||||
transparency: "none",
|
||||
windowMaterial: "mica",
|
||||
spellcheck: true,
|
||||
hardwareAcceleration: true,
|
||||
sdpH264BaselineRewrite: true,
|
||||
performanceMode: "none",
|
||||
skipSplash: false,
|
||||
inviteWebsocket: true,
|
||||
|
|
@ -81,6 +82,7 @@ const safeMode: Settings = {
|
|||
windowStyle: "native",
|
||||
csp: "vanilla",
|
||||
hardwareAcceleration: false,
|
||||
sdpH264BaselineRewrite: false,
|
||||
disableHttpCache: true,
|
||||
vaapi: false,
|
||||
additionalArguments: "",
|
||||
|
|
@ -120,6 +122,11 @@ export function getConfig<K extends keyof Settings>(object: K): Settings[K] {
|
|||
configCacheTime = now;
|
||||
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 {
|
||||
const rawData = readFileSync(getConfigLocation(), "utf-8");
|
||||
const parsed = JSON.parse(rawData) as Settings;
|
||||
|
|
|
|||
68
src/dbus.ts
68
src/dbus.ts
|
|
@ -1,7 +1,13 @@
|
|||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { interface as dbusInterface, type ProxyInterface, sessionBus, Variant } from "@jellybrick/dbus-next";
|
||||
import {
|
||||
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";
|
||||
|
||||
|
|
@ -13,6 +19,8 @@ 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 {
|
||||
|
|
@ -74,8 +82,16 @@ class LegcordInterface extends Interface {
|
|||
}
|
||||
}
|
||||
|
||||
const bus = sessionBus();
|
||||
var legcordInterface = new LegcordInterface();
|
||||
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");
|
||||
|
|
@ -88,14 +104,18 @@ function ensureDesktopFile(): void {
|
|||
|
||||
function registerAppId(): Promise<void> {
|
||||
ensureDesktopFile(); // we need a desktop file at XDG_PATH so we can associate with legcord's app_id
|
||||
return bus.getProxyObject("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop").then((portalObj) => {
|
||||
const registry: RegistryInterface = portalObj.getInterface("org.freedesktop.host.portal.Registry");
|
||||
return registry.Register(DBUS_INTERFACE_NAME, {});
|
||||
});
|
||||
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() {
|
||||
const portalObj = await bus.getProxyObject(FREEDESKTOP_PORTAL_NAME, FREEDESKTOP_PORTAL_ADDRESS);
|
||||
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");
|
||||
|
|
@ -108,17 +128,19 @@ export async function setupGlobalShortcuts() {
|
|||
console.debug(`Registered ${DBUS_INTERFACE_NAME} in freedesktop Registry.`);
|
||||
|
||||
function awaitResponse(requestPath: string): Promise<Record<string, unknown>> {
|
||||
return bus.getProxyObject(FREEDESKTOP_PORTAL_NAME, requestPath).then(
|
||||
(requestObj) =>
|
||||
new Promise<Record<string, unknown>>((resolve, reject) => {
|
||||
const requestIface = requestObj.getInterface("org.freedesktop.portal.Request");
|
||||
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}`));
|
||||
});
|
||||
}),
|
||||
);
|
||||
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({
|
||||
|
|
@ -146,13 +168,19 @@ export async function setupGlobalShortcuts() {
|
|||
}
|
||||
|
||||
export async function startDbusService(): Promise<void> {
|
||||
await bus.requestName(DBUS_INTERFACE_NAME);
|
||||
bus.export(DBUS_ADDRESS, legcordInterface);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
unblacklistGame as blacklistGameRemove,
|
||||
getBlacklist,
|
||||
} from "../common/blacklistGame.js";
|
||||
import { getConfig, getConfigLocation, setConfig, setConfigBulk } from "../common/config.js";
|
||||
import { getConfig, getConfigLocation, setConfig, setConfigBulk, shouldStartMinimized } from "../common/config.js";
|
||||
import { addDetectable, getDetectables, removeDetectable } from "../common/detectables.js";
|
||||
import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js";
|
||||
import {
|
||||
|
|
@ -217,8 +217,8 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
|||
});
|
||||
|
||||
ipcMain.on("splashEnd", () => {
|
||||
splashWindow.close();
|
||||
if (getConfig("startMinimized")) {
|
||||
splashWindow?.close();
|
||||
if (shouldStartMinimized()) {
|
||||
passedWindow.hide();
|
||||
} else {
|
||||
passedWindow.show();
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
|
|||
// 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";
|
||||
|
|
@ -45,7 +49,7 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
|
|||
var MIN_BR = "3000";
|
||||
var START_BR = "3000";
|
||||
// VideoToolbox (macOS) and Media Foundation (Windows) HW encode avoid OpenH264 CBP.
|
||||
var preferHwH264 = ${process.platform === "darwin" || process.platform === "win32" ? "true" : "false"};
|
||||
var preferHwH264 = ${preferHwH264 ? "true" : "false"};
|
||||
function setOrAppendFmtpParam(sdp, key, value) {
|
||||
var re = new RegExp(key + "=\\\\d+", "g");
|
||||
if (sdp.indexOf(key + "=") !== -1) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
shell,
|
||||
} from "electron";
|
||||
import contextMenu from "electron-context-menu";
|
||||
import { firstRun, getConfig, setConfig } from "../common/config.js";
|
||||
import { firstRun, getConfig, setConfig, shouldStartMinimized } from "../common/config.js";
|
||||
import { navigateTo } from "../common/dom.js";
|
||||
import { forceQuit, setForceQuit } from "../common/forceQuit.js";
|
||||
import { handleCommands, passedValidArgument } from "../common/handleCommands.js";
|
||||
|
|
@ -474,8 +474,13 @@ function doAfterDefiningTheWindow(passedWindow: BrowserWindow): void {
|
|||
break;
|
||||
}
|
||||
|
||||
if (getConfig("skipSplash")) {
|
||||
passedWindow.show();
|
||||
// When splash won't run, finalize visibility here (splashEnd never fires).
|
||||
if (getConfig("skipSplash") || shouldStartMinimized()) {
|
||||
if (shouldStartMinimized()) {
|
||||
passedWindow.hide();
|
||||
} else {
|
||||
passedWindow.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
setConfig,
|
||||
setFirstRun,
|
||||
setup,
|
||||
shouldStartMinimized,
|
||||
} from "./common/config.js";
|
||||
import { getPreset } from "./common/flags.js";
|
||||
import { setLang } from "./common/lang.js";
|
||||
|
|
@ -108,7 +109,9 @@ function args(): void {
|
|||
export async function init(): Promise<void> {
|
||||
// Skip setup if bypass flag was used
|
||||
if (bypassSetup || !(firstRun === true || undefined)) {
|
||||
if (getConfig("skipSplash") === false) {
|
||||
// Splash owns the final show/hide via splashEnd; skip it when splash is disabled
|
||||
// or when we intentionally start in the background.
|
||||
if (getConfig("skipSplash") === false && !shouldStartMinimized()) {
|
||||
void createSplashWindow(); // NOTE - Awaiting will hang at start
|
||||
}
|
||||
createWindow();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export async function createSetupWindow(): Promise<void> {
|
|||
if (platform() !== "darwin") import("./tray.js");
|
||||
return new Promise(() => {
|
||||
const windowOptions: BrowserWindowConstructorOptions = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
width: 840,
|
||||
height: 720,
|
||||
title: getLang("setup-windowTitle"),
|
||||
darkTheme: true,
|
||||
icon: getConfig("customIcon") ?? path.join(import.meta.dirname, "../", "/assets/desktop.png"),
|
||||
|
|
|
|||
|
|
@ -15,17 +15,7 @@ contextBridge.exposeInMainWorld("setup", {
|
|||
|
||||
if (ipcRenderer.sendSync("setup-getOS") !== "darwin") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
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);
|
||||
document.body.classList.add("has-setup-bg");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
488
src/setup/setup.css
Normal file
488
src/setup/setup.css
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
: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;
|
||||
}
|
||||
|
|
@ -4,18 +4,8 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Legcord Setup</title>
|
||||
<style>
|
||||
.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>
|
||||
<link rel="stylesheet" href="compat.css">
|
||||
<link rel="stylesheet" href="setup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="draggable"></div>
|
||||
|
|
@ -23,4 +13,4 @@
|
|||
<div id="root"></div>
|
||||
<script type="module" src="setup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,122 +1,167 @@
|
|||
import {
|
||||
Box,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
LaptopMinimal,
|
||||
LaptopMinimalCheck,
|
||||
MinimizeIcon,
|
||||
Monitor,
|
||||
PuzzleIcon,
|
||||
Settings,
|
||||
Sparkles,
|
||||
} from "lucide-solid";
|
||||
import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import { Button, ButtonColors, ButtonSizes, injectInternalStyles, niceScrollbarsClass } from "@uwu/shelter-ui";
|
||||
import { type Accessor, createMemo, createResource, createSignal, For, onMount, Show } from "solid-js";
|
||||
import { render } from "solid-js/web";
|
||||
import { Motion } from "solid-motionone";
|
||||
|
||||
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">
|
||||
<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">
|
||||
{t()?.["setup-welcomeTitle"] ?? "Welcome to Legcord"}
|
||||
</h1>
|
||||
<p class="text-gray-400 mb-8">
|
||||
injectInternalStyles();
|
||||
|
||||
type Lang = Record<string, string> | undefined;
|
||||
|
||||
const ctaStyle = {
|
||||
"--shltr-btn-w": "280px",
|
||||
"--shltr-btn-h": "44px",
|
||||
"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."}
|
||||
</p>
|
||||
<Motion.button
|
||||
<Button
|
||||
color={ButtonColors.PRIMARY}
|
||||
size={ButtonSizes.LARGE}
|
||||
onClick={onNext}
|
||||
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"
|
||||
class="setup-cta"
|
||||
style={ctaStyle}
|
||||
>
|
||||
{t()?.["setup-getStarted"] ?? "Get Started"}
|
||||
</Motion.button>
|
||||
</Button>
|
||||
</Motion.div>
|
||||
);
|
||||
|
||||
const WindowStyle = ({
|
||||
readyToNext,
|
||||
selectedStyle,
|
||||
onSelect,
|
||||
t,
|
||||
}: {
|
||||
readyToNext: (valid: boolean) => void;
|
||||
t: () => Record<string, string> | undefined;
|
||||
selectedStyle: Accessor<string>;
|
||||
onSelect: (styleId: string) => void;
|
||||
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 = [
|
||||
{
|
||||
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",
|
||||
titleKey: "setup-windowStyle-nativeTitle",
|
||||
titleFallback: "Native Window",
|
||||
descKey: "setup-windowStyle-nativeDesc",
|
||||
descFallback: "Use your system's default window decorations",
|
||||
screenshot: "legcord://assets/native.png",
|
||||
},
|
||||
{
|
||||
id: "default",
|
||||
titleKey: "setup-windowStyle-customTitle",
|
||||
titleFallback: "Custom Titlebar",
|
||||
descKey: "setup-windowStyle-customDesc",
|
||||
descFallback: "Use Legcord's custom titlebar design",
|
||||
screenshot: "legcord://assets/custom.png",
|
||||
},
|
||||
];
|
||||
const lang = t();
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<div class="text-center">
|
||||
<Monitor class="w-8 h-8 text-purple-400 mx-auto mb-4" />
|
||||
<h2 class="text-2xl font-bold text-white mb-2">
|
||||
{lang?.["setup-chooseWindowStyle"] ?? "Choose Window Style"}
|
||||
</h2>
|
||||
<p class="text-gray-400">
|
||||
<div class="setup-step">
|
||||
<div class="setup-header">
|
||||
<h2 class="setup-title">{lang?.["setup-chooseWindowStyle"] ?? "Choose Window Style"}</h2>
|
||||
<p class="setup-subtitle">
|
||||
{lang?.["setup-selectAppearance"] ?? "Select how Legcord appears on your machine"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="setup-options">
|
||||
<For each={styles}>
|
||||
{(item) => (
|
||||
<Motion.button
|
||||
onClick={() => handleStyleSelect(item.id)}
|
||||
class={`group relative w-full p-4 rounded-xl transition-all duration-300 text-left ${
|
||||
selectedStyle() === item.id
|
||||
? "bg-purple-900/40 border border-purple-500/50 shadow-lg shadow-purple-500/20"
|
||||
: "bg-gray-800/40 border border-gray-700/30 hover:bg-gray-800/60"
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
<OptionCard
|
||||
selected={selectedStyle() === item.id}
|
||||
onSelect={() => onSelect(item.id)}
|
||||
title={lang?.[item.titleKey] ?? item.titleFallback}
|
||||
description={lang?.[item.descKey] ?? item.descFallback}
|
||||
badge={item.recommended ? (lang?.["setup-recommended"] ?? "Recommended") : undefined}
|
||||
preview={item.screenshot}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
|
@ -125,224 +170,166 @@ const WindowStyle = ({
|
|||
};
|
||||
|
||||
const TraySettings = ({
|
||||
readyToNext,
|
||||
selectedOption,
|
||||
onSelect,
|
||||
t,
|
||||
}: {
|
||||
readyToNext: (valid: boolean) => void;
|
||||
t: () => Record<string, string> | undefined;
|
||||
selectedOption: Accessor<string | null>;
|
||||
onSelect: (optionId: string) => void;
|
||||
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 = [
|
||||
{ id: "dynamic", titleKey: "setup-trayEnableTitle", descKey: "setup-trayEnableDesc", icon: LaptopMinimalCheck },
|
||||
{ id: "disabled", titleKey: "setup-trayDisableTitle", descKey: "setup-trayDisableDesc", icon: LaptopMinimal },
|
||||
{
|
||||
id: "dynamic",
|
||||
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();
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<div class="text-center">
|
||||
<MinimizeIcon class="w-8 h-8 text-purple-400 mx-auto mb-4" />
|
||||
<h2 class="text-2xl font-bold text-white mb-2">{lang?.["setup-systemTray"] ?? "System Tray"}</h2>
|
||||
<p class="text-gray-400">
|
||||
<div class="setup-step">
|
||||
<div class="setup-header">
|
||||
<h2 class="setup-title">{lang?.["setup-systemTray"] ?? "System Tray"}</h2>
|
||||
<p class="setup-subtitle">
|
||||
{lang?.["setup-trayChoose"] ?? "Choose whether to enable the system tray icon"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={window.setup.os === "linux"}>
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 rounded-xl bg-yellow-500/10 border border-yellow-500/20 flex items-center gap-3">
|
||||
<CircleAlert class="w-5 h-5 text-yellow-400 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-yellow-200/80 text-sm mt-0.5">
|
||||
{lang?.["setup-linuxTrayWarning"] ??
|
||||
"System tray functionality may have issues or behave differently on Linux systems."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-warning">
|
||||
<WarningIcon />
|
||||
<span>
|
||||
{lang?.["setup-linuxTrayWarning"] ??
|
||||
"System tray functionality may have issues or behave differently on Linux systems."}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<For each={options}>
|
||||
{(option) => (
|
||||
<Motion.button
|
||||
onClick={() => handleOptionSelect(option.id)}
|
||||
class={`group relative w-full p-4 rounded-xl transition-all duration-300 text-left ${
|
||||
selectedOption() === option.id
|
||||
? "bg-purple-900/40 border border-purple-500/50 shadow-lg shadow-purple-500/20"
|
||||
: "bg-gray-800/40 border border-gray-700/30 hover:bg-gray-800/60"
|
||||
}`}
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<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 class="setup-options">
|
||||
<For each={options}>
|
||||
{(option) => (
|
||||
<OptionCard
|
||||
selected={selectedOption() === option.id}
|
||||
onSelect={() => onSelect(option.id)}
|
||||
title={lang?.[option.titleKey] ?? option.titleFallback}
|
||||
description={lang?.[option.descKey] ?? option.descFallback}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Finish = ({ restart, t }: { restart: () => void; t: () => Record<string, string> | undefined }) => (
|
||||
<div class="text-center">
|
||||
<Box class="w-8 h-8 text-purple-400 mx-auto mb-4" />
|
||||
<h2 class="text-2xl font-bold text-white mb-4">{t()?.["setup-finishTitle"] ?? "You're All Set!"}</h2>
|
||||
<p class="text-gray-400 mb-6">
|
||||
const Finish = ({ restart, t }: { restart: () => void; t: () => Lang }) => (
|
||||
<Motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} class="setup-finish">
|
||||
<div class="setup-finish-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" role="presentation">
|
||||
<circle cx="12" cy="12" r="12" fill="var(--brand-500)" />
|
||||
<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"] ??
|
||||
"Your Legcord configuration is complete and personalized to your preferences."}
|
||||
</p>
|
||||
<div class="p-4 rounded-xl bg-gray-800/40 border border-gray-700/30 mb-6">
|
||||
<Settings class="w-6 h-6 text-purple-400 mx-auto mb-2" />
|
||||
<p class="text-sm text-gray-400">
|
||||
<div class="setup-note">
|
||||
<p>
|
||||
{t()?.["setup-finishSettingsNote"] ??
|
||||
"Need to make changes later? You'll find all these options in Discord's settings menu under Legcord."}
|
||||
</p>
|
||||
</div>
|
||||
<Motion.button
|
||||
<Button
|
||||
color={ButtonColors.PRIMARY}
|
||||
size={ButtonSizes.LARGE}
|
||||
onClick={restart}
|
||||
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"
|
||||
class="setup-cta"
|
||||
style={ctaStyle}
|
||||
>
|
||||
{t()?.["setup-launchLegcord"] ?? "Launch Legcord"}
|
||||
</Motion.button>
|
||||
</div>
|
||||
</Button>
|
||||
</Motion.div>
|
||||
);
|
||||
|
||||
const ModSelector = ({
|
||||
readyToNext,
|
||||
selectedMod,
|
||||
onSelect,
|
||||
t,
|
||||
}: {
|
||||
readyToNext: (valid: boolean) => void;
|
||||
t: () => Record<string, string> | undefined;
|
||||
selectedMod: Accessor<string>;
|
||||
onSelect: (modId: string) => void;
|
||||
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 = [
|
||||
{
|
||||
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",
|
||||
titleKey: "setup-vencordTitle",
|
||||
titleFallback: "Vencord",
|
||||
descKey: "setup-vencordDesc",
|
||||
descFallback: "Client mod with plugins and themes.",
|
||||
icon: "legcord://assets/vencord.png",
|
||||
},
|
||||
{
|
||||
id: "equicord",
|
||||
titleKey: "setup-equicordTitle",
|
||||
titleFallback: "Equicord",
|
||||
descKey: "setup-equicordDesc",
|
||||
descFallback: "A fork of Vencord with more plugins.",
|
||||
icon: "legcord://assets/equicord.png",
|
||||
},
|
||||
];
|
||||
const lang = t();
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<div class="text-center">
|
||||
<PuzzleIcon class="w-8 h-8 text-purple-400 mx-auto mb-4" />
|
||||
<h2 class="text-2xl font-bold text-white mb-2">
|
||||
{lang?.["setup-modSelectorTitle"] ?? "Choose Your Client Mod"}
|
||||
</h2>
|
||||
<p class="text-gray-400">
|
||||
<div class="setup-step">
|
||||
<div class="setup-header">
|
||||
<h2 class="setup-title">{lang?.["setup-modSelectorTitle"] ?? "Choose Your Client Mod"}</h2>
|
||||
<p class="setup-subtitle">
|
||||
{lang?.["setup-modSelectorSubtitle"] ??
|
||||
"Legcord includes Shelter out of the box, but you can also choose another client mod if wanted."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<For each={mods} fallback={<div>{lang?.["setup-loading"] ?? "Loading..."}</div>}>
|
||||
<div class="setup-options">
|
||||
<For each={mods}>
|
||||
{(mod) => (
|
||||
<Motion.button
|
||||
onClick={() => handleModSelect(mod.id)}
|
||||
class={`group relative w-full p-4 rounded-xl transition-all duration-300 text-left ${
|
||||
selectedMod() === mod.id
|
||||
? "bg-purple-900/40 border border-purple-500/50 shadow-lg shadow-purple-500/20"
|
||||
: "bg-gray-800/40 border border-gray-700/30 hover:bg-gray-800/60"
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
<OptionCard
|
||||
selected={selectedMod() === mod.id}
|
||||
onSelect={() => onSelect(mod.id)}
|
||||
title={lang?.[mod.titleKey] ?? mod.titleFallback}
|
||||
description={lang?.[mod.descKey] ?? mod.descFallback}
|
||||
badge={mod.recommended ? (lang?.["setup-recommended"] ?? "Recommended") : undefined}
|
||||
icon={mod.icon}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -350,84 +337,117 @@ const ModSelector = ({
|
|||
function Stepper() {
|
||||
const [t] = createResource(() => window.setup.getRawLang());
|
||||
const [currentStep, setCurrentStep] = createSignal(0);
|
||||
const [isValid, setValid] = createSignal(true);
|
||||
const [selectedStyle, setSelectedStyle] = createSignal("overlay");
|
||||
const [selectedMod, setSelectedMod] = createSignal("shelter");
|
||||
const [selectedTray, setSelectedTray] = createSignal<string | null>(null);
|
||||
const maxSteps = 5;
|
||||
const handleNext = () => {
|
||||
if (!isValid()) return;
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
setValid(false);
|
||||
|
||||
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 handleBack = () => {
|
||||
setCurrentStep((prev) => prev - 1);
|
||||
if (currentStep() === 0) {
|
||||
setValid(true);
|
||||
|
||||
const handleModSelect = (modId: string) => {
|
||||
setSelectedMod(modId);
|
||||
if (modId !== "shelter") {
|
||||
window.setup.saveSettings({ mods: [modId] });
|
||||
} else {
|
||||
window.setup.saveSettings({ mods: [] });
|
||||
}
|
||||
};
|
||||
const setReady = (valid: boolean) => {
|
||||
setValid(valid);
|
||||
|
||||
const handleTraySelect = (trayId: string) => {
|
||||
setSelectedTray(trayId);
|
||||
window.setup.saveSettings({ tray: trayId });
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!canProceed()) return;
|
||||
setCurrentStep((prev) => Math.min(prev + 1, maxSteps - 1));
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 0));
|
||||
};
|
||||
|
||||
const restart = () => {
|
||||
console.log("Restarting...");
|
||||
window.setup.saveSettings({ doneSetup: true });
|
||||
window.setup.restart();
|
||||
};
|
||||
|
||||
const stepOfText = () => {
|
||||
const lang = t();
|
||||
const template = lang?.["setup-stepOf"] ?? "Step {current} of {total}";
|
||||
return template.replace("{current}", String(currentStep() + 1)).replace("{total}", String(maxSteps));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<Motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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"
|
||||
>
|
||||
<Motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} class="setup-shell">
|
||||
<Show when={currentStep() !== 0 && currentStep() !== 4}>
|
||||
<Progress current={currentStep()} total={maxSteps} />
|
||||
</Show>
|
||||
|
||||
<div class={`setup-body ${niceScrollbarsClass()}`}>
|
||||
<Show when={currentStep() === 0}>
|
||||
<Welcome onNext={handleNext} t={t} />
|
||||
</Show>
|
||||
<Show when={currentStep() === 1}>
|
||||
<WindowStyle readyToNext={setReady} t={t} />
|
||||
<WindowStyle selectedStyle={selectedStyle} onSelect={handleStyleSelect} t={t} />
|
||||
</Show>
|
||||
<Show when={currentStep() === 2}>
|
||||
<ModSelector readyToNext={setReady} t={t} />
|
||||
<ModSelector selectedMod={selectedMod} onSelect={handleModSelect} t={t} />
|
||||
</Show>
|
||||
<Show when={currentStep() === 3}>
|
||||
<TraySettings readyToNext={setReady} t={t} />
|
||||
<TraySettings selectedOption={selectedTray} onSelect={handleTraySelect} t={t} />
|
||||
</Show>
|
||||
<Show when={currentStep() === 4}>
|
||||
<Finish restart={restart} t={t} />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={currentStep() !== 0 && currentStep() !== 4}>
|
||||
<div class="mt-8 flex items-center justify-between">
|
||||
<Motion.button
|
||||
onClick={handleBack}
|
||||
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"
|
||||
>
|
||||
<ChevronLeft class="w-4 h-4" />
|
||||
{t()?.["setup-back"] ?? "Back"}
|
||||
</Motion.button>
|
||||
|
||||
<div class="px-4 py-1 rounded-full bg-gray-800/50 text-gray-400 font-medium text-sm">
|
||||
{stepOfText()}
|
||||
</div>
|
||||
|
||||
<Motion.button
|
||||
onClick={handleNext}
|
||||
class={`px-6 py-2.5 rounded-xl text-white font-medium transition-colors inline-flex items-center gap-2 ${
|
||||
isValid()
|
||||
? "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"}
|
||||
<ChevronRight class="w-4 h-4" />
|
||||
</Motion.button>
|
||||
</div>
|
||||
</Show>
|
||||
</Motion.div>
|
||||
</div>
|
||||
<Show when={currentStep() !== 0 && currentStep() !== 4}>
|
||||
<div class="setup-footer">
|
||||
<Button
|
||||
color={ButtonColors.SECONDARY}
|
||||
size={ButtonSizes.MEDIUM}
|
||||
onClick={handleBack}
|
||||
class="setup-footer-btn"
|
||||
style={footerBtnStyle}
|
||||
>
|
||||
{t()?.["setup-back"] ?? "Back"}
|
||||
</Button>
|
||||
<span class="setup-step-indicator">{stepOfText()}</span>
|
||||
<Button
|
||||
color={ButtonColors.PRIMARY}
|
||||
size={ButtonSizes.MEDIUM}
|
||||
disabled={!canProceed()}
|
||||
onClick={handleNext}
|
||||
class="setup-footer-btn"
|
||||
style={footerBtnStyle}
|
||||
>
|
||||
{t()?.["setup-next"] ?? "Next"}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</Motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export function SettingsPage() {
|
|||
t["settings-venmic-workaround"],
|
||||
t["settings-audio"],
|
||||
t["settings-hardwareAcceleration"],
|
||||
t["settings-sdpH264BaselineRewrite"],
|
||||
t["settings-vaapi"],
|
||||
t["settings-automaticClientUpdates"],
|
||||
t["settings-disableHttpCache"],
|
||||
|
|
@ -488,7 +489,7 @@ export function SettingsPage() {
|
|||
<SwitchItem
|
||||
note={t["settings-startMinimized-desc"]}
|
||||
value={settings.startMinimized}
|
||||
onChange={(e: boolean) => setConfig("startMinimized", e)}
|
||||
onChange={(e: boolean) => setConfig("startMinimized", e, true)}
|
||||
>
|
||||
{t["settings-startMinimized"]}
|
||||
</SwitchItem>
|
||||
|
|
@ -856,6 +857,26 @@ export function SettingsPage() {
|
|||
{t["settings-hardwareAcceleration"]}
|
||||
</SwitchItem>
|
||||
</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"}>
|
||||
<SearchableSetting keywords={[t["settings-vaapi"], t["settings-vaapi-desc"], "VAAPI"]}>
|
||||
<SwitchItem
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import path from "node:path";
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import isDev from "electron-is-dev";
|
||||
import { getConfig } from "../common/config.js";
|
||||
import { getConfig, shouldStartMinimized } from "../common/config.js";
|
||||
import { getLang } from "../common/lang.js";
|
||||
|
||||
export let splashWindow: BrowserWindow;
|
||||
export async function createSplashWindow(): Promise<void> {
|
||||
if (getConfig("startMinimized")) return;
|
||||
if (shouldStartMinimized()) return;
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 350,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue